storkit: merge 448_story_send_oauth_login_link_via_chat_when_credentials_are_missing

This commit is contained in:
dave
2026-03-31 10:24:37 +00:00
parent 762467efd4
commit 5516ec4595
5 changed files with 108 additions and 80 deletions
Generated
+21 -76
View File
@@ -26,7 +26,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common 0.1.7",
"crypto-common",
"generic-array",
]
@@ -286,15 +286,6 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block-buffer"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
dependencies = [
"hybrid-array",
]
[[package]]
name = "block-padding"
version = "0.3.3"
@@ -436,7 +427,7 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common 0.1.7",
"crypto-common",
"inout",
"zeroize",
]
@@ -501,12 +492,6 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "const_panic"
version = "0.2.15"
@@ -620,15 +605,6 @@ dependencies = [
"typenum",
]
[[package]]
name = "crypto-common"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710"
dependencies = [
"hybrid-array",
]
[[package]]
name = "ctr"
version = "0.9.2"
@@ -647,7 +623,7 @@ dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"curve25519-dalek-derive",
"digest 0.10.7",
"digest",
"fiat-crypto",
"rustc_version",
"serde",
@@ -773,7 +749,7 @@ version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid 0.9.6",
"const-oid",
"zeroize",
]
@@ -846,22 +822,11 @@ version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer 0.10.4",
"crypto-common 0.1.7",
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "digest"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c"
dependencies = [
"block-buffer 0.12.0",
"const-oid 0.10.2",
"crypto-common 0.2.1",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -906,7 +871,7 @@ dependencies = [
"ed25519",
"rand_core 0.6.4",
"serde",
"sha2 0.10.9",
"sha2",
"subtle",
"zeroize",
]
@@ -1415,7 +1380,7 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest 0.10.7",
"digest",
]
[[package]]
@@ -1495,15 +1460,6 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hybrid-array"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a79f2aff40c18ab8615ddc5caa9eb5b96314aef18fe5823090f204ad988e813"
dependencies = [
"typenum",
]
[[package]]
name = "hyper"
version = "1.8.1"
@@ -2211,7 +2167,7 @@ dependencies = [
"serde",
"serde_html_form",
"serde_json",
"sha2 0.10.9",
"sha2",
"tempfile",
"thiserror 2.0.18",
"tokio",
@@ -2304,7 +2260,7 @@ dependencies = [
"ruma",
"serde",
"serde_json",
"sha2 0.10.9",
"sha2",
"subtle",
"thiserror 2.0.18",
"time",
@@ -2339,7 +2295,7 @@ dependencies = [
"serde",
"serde-wasm-bindgen",
"serde_json",
"sha2 0.10.9",
"sha2",
"thiserror 2.0.18",
"tokio",
"tracing",
@@ -2393,7 +2349,7 @@ dependencies = [
"rmp-serde",
"serde",
"serde_json",
"sha2 0.10.9",
"sha2",
"thiserror 2.0.18",
"zeroize",
]
@@ -2652,7 +2608,7 @@ dependencies = [
"serde",
"serde_json",
"serde_path_to_error",
"sha2 0.10.9",
"sha2",
"thiserror 1.0.69",
"url",
]
@@ -2710,7 +2666,7 @@ version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest 0.10.7",
"digest",
"hmac",
]
@@ -3557,7 +3513,7 @@ dependencies = [
"rand 0.8.5",
"ruma-common",
"serde_json",
"sha2 0.10.9",
"sha2",
"thiserror 2.0.18",
]
@@ -3605,7 +3561,7 @@ version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
dependencies = [
"sha2 0.10.9",
"sha2",
"walkdir",
]
@@ -3930,7 +3886,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"digest 0.10.7",
"digest",
]
[[package]]
@@ -3941,18 +3897,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"digest 0.10.7",
]
[[package]]
name = "sha2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"digest 0.11.2",
"digest",
]
[[package]]
@@ -4110,7 +4055,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"serde_yaml",
"sha2 0.11.0",
"sha2",
"strip-ansi-escapes",
"tempfile",
"tokio",
@@ -4745,7 +4690,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common 0.1.7",
"crypto-common",
"subtle",
]
@@ -4845,7 +4790,7 @@ dependencies = [
"serde",
"serde_bytes",
"serde_json",
"sha2 0.10.9",
"sha2",
"subtle",
"thiserror 2.0.18",
"x25519-dalek",
@@ -616,7 +616,13 @@ pub(super) async fn handle_message(
}
Err(e) => {
slog!("[matrix-bot] LLM error: {e}");
let err_msg = format!("Error processing your request: {e}");
let err_msg = if let Some(url) = crate::llm::oauth::extract_login_url_from_error(&e) {
format!(
"Authentication required. [Click here to log in to Claude]({url})"
)
} else {
format!("Error processing your request: {e}")
};
let _ = msg_tx.send(err_msg.clone());
(err_msg, None)
}
@@ -686,6 +692,24 @@ mod tests {
assert_eq!(prompt, "@bob:example.com: What's up?");
}
// -- OAuth login link formatting ----------------------------------------
#[test]
fn oauth_error_produces_login_link() {
let err = "OAuth session expired or credentials missing. Please log in: http://localhost:3001/oauth/authorize";
let url = crate::llm::oauth::extract_login_url_from_error(err);
assert!(url.is_some(), "should extract URL from OAuth error");
let msg = format!("Authentication required. [Click here to log in to Claude]({})", url.unwrap());
assert!(msg.contains("http://localhost:3001/oauth/authorize"));
assert!(msg.contains("[Click here to log in to Claude]"));
}
#[test]
fn non_oauth_error_not_formatted_as_link() {
let err = "Some unrelated error";
assert!(crate::llm::oauth::extract_login_url_from_error(err).is_none());
}
// -- bot_name / system prompt -------------------------------------------
#[test]
+25 -1
View File
@@ -383,7 +383,13 @@ async fn handle_llm_message(ctx: &WhatsAppWebhookContext, sender: &str, user_mes
}
Err(e) => {
slog!("[whatsapp] LLM error: {e}");
let err_msg = format!("Error processing your request: {e}");
let err_msg = if let Some(url) = crate::llm::oauth::extract_login_url_from_error(&e) {
format!(
"Authentication required. Log in to Claude here: {url}"
)
} else {
format!("Error processing your request: {e}")
};
let _ = msg_tx.send(err_msg.clone());
(err_msg, None)
}
@@ -491,6 +497,24 @@ mod tests {
})
}
// ── OAuth login link formatting ───────────────────────────────────────
#[test]
fn whatsapp_oauth_error_produces_plain_text_url() {
let err = "OAuth session expired or credentials missing. Please log in: http://localhost:3001/oauth/authorize";
let url = crate::llm::oauth::extract_login_url_from_error(err);
assert!(url.is_some(), "should extract URL from OAuth error");
let msg = format!("Authentication required. Log in to Claude here: {}", url.unwrap());
assert!(msg.contains("http://localhost:3001/oauth/authorize"));
assert!(!msg.contains('['), "WhatsApp message should not use Markdown link syntax");
}
#[test]
fn whatsapp_non_oauth_error_not_formatted_as_link() {
let err = "Some unrelated error occurred during processing";
assert!(crate::llm::oauth::extract_login_url_from_error(err).is_none());
}
// ── Allowlist tests ───────────────────────────────────────────────────
#[tokio::test]
+33
View File
@@ -161,10 +161,43 @@ pub async fn refresh_access_token() -> Result<(), String> {
Ok(())
}
/// Extract the OAuth login URL from an error message produced by the Claude Code provider.
///
/// The provider returns errors like:
/// `"OAuth session expired or credentials missing. Please log in: http://localhost:3001/oauth/authorize"`
///
/// Returns the URL portion when the error indicates missing or expired credentials,
/// `None` otherwise.
pub fn extract_login_url_from_error(err: &str) -> Option<&str> {
let marker = "Please log in: ";
let start = err.find(marker)?;
Some(err[start + marker.len()..].trim())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_login_url_from_oauth_error() {
let err = "OAuth session expired or credentials missing. Please log in: http://localhost:3001/oauth/authorize";
let url = extract_login_url_from_error(err);
assert_eq!(url, Some("http://localhost:3001/oauth/authorize"));
}
#[test]
fn extract_login_url_returns_none_for_unrelated_error() {
let err = "Some other error occurred";
assert!(extract_login_url_from_error(err).is_none());
}
#[test]
fn extract_login_url_with_different_port() {
let err = "OAuth session expired or credentials missing. Please log in: http://localhost:3002/oauth/authorize";
let url = extract_login_url_from_error(err);
assert_eq!(url, Some("http://localhost:3002/oauth/authorize"));
}
#[test]
fn parse_credentials_file() {
let json = r#"{
+4 -2
View File
@@ -138,9 +138,11 @@ impl ClaudeCodeProvider {
on_token("\n*Refreshing authentication token...*\n");
continue;
}
Err(e) => {
Err(_e) => {
let port = crate::http::resolve_port();
let login_url = format!("http://localhost:{port}/oauth/authorize");
return Err(format!(
"OAuth session expired. Please run `claude login` to re-authenticate. ({e})"
"OAuth session expired or credentials missing. Please log in: {login_url}"
));
}
}