g1t/crates/contracts/src/names.rs

112 lines3,279 bytesCodeBlame
1/// Routes and reserved words that may not be registered as usernames.
2const RESERVED: &[&str] = &[
3 "api",
4 "mcp",
5 "login",
6 "logout",
7 "register",
8 "new",
9 "settings",
10 "search",
11 "admin",
12 "auth",
13 // g1t.sh/integrations/github/setup: where GitHub returns after an install.
14 "integrations",
15 "pulls",
16 "issues",
17 "oauth",
18 "assets",
19 "avatars",
20 "docs",
21 "explore",
22 // g1t itself, and the name its agent once went by: everything g1t
23 // does is shown as `g1t`, so nobody else may be called either.
24 "g1t",
25 "g1t-agent",
26 "about",
27 "pricing",
28 "terms",
29 "privacy",
30 "policies",
31 "security",
32 "help",
33 "support",
34 "status",
35 "blog",
36 "verify",
37 "forgot",
38 "reset",
39 "device",
40 "workspaces",
41 "u",
42 // g1t.sh/invite/<code>: invite links; and the waitlist.
43 "invite",
44 "invites",
45 "waitlist",
46];
47
48/// Whether `value`, whatever its case, is a name nobody can register or
49/// rename a workspace to: a route, or g1t's own.
50pub fn is_reserved_name(value: &str) -> bool {
51 let value = value.trim();
52 RESERVED.iter().any(|reserved| reserved.eq_ignore_ascii_case(value))
53}
54
55/// A username or workspace slug as someone typed it, trimmed and
56/// lowercased, if it can be registered: what signing up, signing up with
57/// GitHub and creating a workspace each take. None when it is malformed or
58/// reserved.
59pub fn claimable_namespace(value: &str) -> Option<String> {
60 let value = value.trim().to_lowercase();
61 is_valid_namespace(&value).then_some(value)
62}
63
64/// Namespaces follow GitHub's rules: letters, digits and single hyphens,
65/// not starting or ending with a hyphen, at most 39 characters.
66pub fn is_valid_namespace(value: &str) -> bool {
67 let bytes = value.as_bytes();
68 !bytes.is_empty()
69 && bytes.len() <= 39
70 && bytes
71 .iter()
72 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'-')
73 && !value.starts_with('-')
74 && !value.ends_with('-')
75 && !value.contains("--")
76 && !is_reserved_name(value)
77}
78
79pub fn is_valid_repo_name(value: &str) -> bool {
80 !value.is_empty()
81 && value.len() <= 100
82 && value.bytes().all(|byte| {
83 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'.' | b'_' | b'-')
84 })
85 && !value.starts_with('.')
86 && !value.ends_with(".git")
87 // `/<workspace>/-/…` holds the workspace's own pages.
88 && value != "-"
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn g1ts_own_names_are_reserved_in_any_case() {
97 for name in ["g1t", "G1T", "g1t-agent", "G1T-Agent", " g1t "] {
98 assert!(is_reserved_name(name), "{name}");
99 assert_eq!(claimable_namespace(name), None, "{name}");
100 }
101 assert!(!is_valid_namespace("g1t"));
102 assert!(!is_valid_namespace("g1t-agent"));
103 }
104
105 #[test]
106 fn names_near_g1ts_are_anyones() {
107 assert!(!is_reserved_name("g1t-agents"));
108 assert_eq!(claimable_namespace("G1T-Bot").as_deref(), Some("g1t-bot"));
109 assert_eq!(claimable_namespace(" Ana ").as_deref(), Some("ana"));
110 assert_eq!(claimable_namespace("an--a"), None);
111 }
112}