codoseo_web/agent/
keys.rs1use crate::auth::session::{hash, random_token};
6use crate::config::Config;
7use crate::error::AppError;
8
9pub const KEY_PREFIX: &str = "cdo_";
11pub const DISPLAY_PREFIX_LEN: usize = 12;
13const KEY_LEN: usize = KEY_PREFIX.len() + 43;
15
16pub struct NewKey {
18 pub plaintext: String,
19 pub hash: Vec<u8>,
20 pub prefix: String,
21}
22
23pub fn generate() -> NewKey {
24 let plaintext = format!("{KEY_PREFIX}{}", random_token());
25 NewKey {
26 hash: hash_key(&plaintext),
27 prefix: plaintext.chars().take(DISPLAY_PREFIX_LEN).collect(),
28 plaintext,
29 }
30}
31
32pub fn hash_key(key: &str) -> Vec<u8> {
34 hash(key)
35}
36
37pub fn is_well_formed(key: &str) -> bool {
40 key.len() == KEY_LEN
41 && key.starts_with(KEY_PREFIX)
42 && key[KEY_PREFIX.len()..]
43 .bytes()
44 .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
45}
46
47pub fn mcp_url(config: &Config) -> Result<String, AppError> {
49 Ok(config
50 .base_url
51 .join("mcp")
52 .map_err(AppError::internal)?
53 .to_string())
54}
55
56pub fn claude_command(mcp_url: &str, key: &str) -> String {
58 format!(
59 "claude mcp add --transport http codoseo {mcp_url} --header \"Authorization: Bearer {key}\""
60 )
61}
62
63pub fn mcp_servers_json(mcp_url: &str, key: &str) -> String {
66 format!(
68 "{{\n \"mcpServers\": {{\n \"codoseo\": {{\n \"type\": \"http\",\n \
69 \"url\": \"{mcp_url}\",\n \"headers\": {{\n \
70 \"Authorization\": \"Bearer {key}\"\n }}\n }}\n }}\n}}"
71 )
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn the_json_snippet_is_valid_and_carries_the_url_and_the_bearer_key() {
80 let key = "cdo_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
81 let json = mcp_servers_json("https://codoseo.com/mcp", key);
82 let v: serde_json::Value = serde_json::from_str(&json).expect("valid JSON");
83 let server = &v["mcpServers"]["codoseo"];
84 assert_eq!(server["type"], "http");
85 assert_eq!(server["url"], "https://codoseo.com/mcp");
86 assert_eq!(server["headers"]["Authorization"], format!("Bearer {key}"));
87 assert!(json.find("\"type\"").unwrap() < json.find("\"url\"").unwrap());
89 assert!(json.find("\"url\"").unwrap() < json.find("\"headers\"").unwrap());
90 }
91
92 #[test]
93 fn the_claude_command_names_the_url_and_the_bearer_key() {
94 assert_eq!(
95 claude_command("https://codoseo.com/mcp", "cdo_x"),
96 "claude mcp add --transport http codoseo https://codoseo.com/mcp --header \"Authorization: Bearer cdo_x\""
97 );
98 }
99
100 #[test]
101 fn a_key_is_cdo_and_43_url_safe_characters() {
102 let key = generate();
103 assert!(key.plaintext.starts_with("cdo_"));
104 assert_eq!(key.plaintext.len(), 4 + 43);
105 assert!(is_well_formed(&key.plaintext));
106 }
107
108 #[test]
109 fn the_hash_is_sha256_of_the_whole_key_and_the_prefix_its_first_12_characters() {
110 let key = generate();
111 assert_eq!(key.hash, hash_key(&key.plaintext));
112 assert_eq!(key.hash.len(), 32);
113 assert_eq!(key.prefix, key.plaintext[..12]);
114 }
115
116 #[test]
117 fn keys_do_not_repeat() {
118 assert_ne!(generate().plaintext, generate().plaintext);
119 assert_ne!(generate().hash, generate().hash);
120 }
121
122 #[test]
123 fn malformed_keys_are_not_well_formed() {
124 for bad in [
125 "",
126 "cdo_",
127 "cdo_short",
128 "xyz_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
129 "cdo_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
130 "cdo_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ",
131 "cdo_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+",
132 ] {
133 assert!(!is_well_formed(bad), "{bad:?}");
134 }
135 assert!(is_well_formed(
136 "cdo_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
137 ));
138 }
139}