From ac152a1219c6db1c0820f711172180fedf3f5b40 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Fri, 24 Oct 2025 11:36:34 +0000 Subject: [PATCH 1/3] feat: Allow dots in organization/scope names Updates validation rules to allow dots (.) in organization names while maintaining backward compatibility with existing names. Backend changes: - Allow dots in ScopeName validation (api/src/ids.rs:30) - Prevent leading/trailing dots (e.g., .org, test.) - Prevent consecutive dots (e.g., foo..bar) - Add new error variants for dot validation - Add comprehensive test coverage for dot patterns Frontend changes: - Update regex to allow dots (frontend/utils/ids.ts:9) - Remove underscore support to match backend validation - Add validation for leading/trailing dots - Add validation for consecutive dots - Update error messages for consistency Valid examples: my.org, foo.bar, foo.bar.baz, org.name-123 Invalid examples: .foo, foo., foo..bar --- api/src/ids.rs | 34 ++++++++++++++++++++++++++++++---- frontend/utils/ids.ts | 10 ++++++++-- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/api/src/ids.rs b/api/src/ids.rs index 871a38c6..66fb307f 100644 --- a/api/src/ids.rs +++ b/api/src/ids.rs @@ -8,8 +8,9 @@ use thiserror::Error; /// A scope name, like `user` or `admin`. The name is not prefixed with an @. /// The name must be at least 2 characters long, and at most 20 characters long. -/// The name must only contain alphanumeric characters and hyphens. -/// The name must not start or end with a hyphen. +/// The name must only contain alphanumeric characters, hyphens, and dots. +/// The name must not start or end with a hyphen or dot. +/// The name must not contain consecutive hyphens or dots. #[derive(Clone, PartialEq, Eq, Hash)] pub struct ScopeName(String); @@ -26,7 +27,7 @@ impl ScopeName { if !name .chars() // temp allow underscores - .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') + .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '.') { return Err(ScopeNameValidateError::InvalidCharacters); } @@ -35,10 +36,18 @@ impl ScopeName { return Err(ScopeNameValidateError::LeadingOrTrailingHyphens); } + if name.starts_with('.') || name.ends_with('.') { + return Err(ScopeNameValidateError::LeadingOrTrailingDots); + } + if name.contains("--") { return Err(ScopeNameValidateError::DoubleHyphens); } + if name.contains("..") { + return Err(ScopeNameValidateError::DoubleDots); + } + Ok(ScopeName(name)) } } @@ -135,15 +144,21 @@ pub enum ScopeNameValidateError { TooLong, #[error( - "scope name must contain only lowercase ascii alphanumeric characters and hyphens" + "scope name must contain only lowercase ascii alphanumeric characters, hyphens, and dots" )] InvalidCharacters, #[error("scope name must not start or end with a hyphen")] LeadingOrTrailingHyphens, + #[error("scope name must not start or end with a dot")] + LeadingOrTrailingDots, + #[error("scope name must not contain double hyphens")] DoubleHyphens, + + #[error("scope name must not contain double dots")] + DoubleDots, } /// A scope description, like 'This is a user scope' or 'Admin scope'. @@ -992,6 +1007,11 @@ mod tests { assert!(ScopeName::try_from("foo-123-bar").is_ok()); assert!(ScopeName::try_from("f123").is_ok()); assert!(ScopeName::try_from("foo-bar-baz-qux").is_ok()); + // Test valid scope names with dots + assert!(ScopeName::try_from("my.org").is_ok()); + assert!(ScopeName::try_from("foo.bar").is_ok()); + assert!(ScopeName::try_from("foo.bar.baz").is_ok()); + assert!(ScopeName::try_from("org.name-123").is_ok()); // Test invalid scope names assert!(ScopeName::try_from("").is_err()); @@ -1007,6 +1027,12 @@ mod tests { assert!(ScopeName::try_from("-123-foo").is_err()); assert!(ScopeName::try_from("foo-123-bar-").is_err()); assert!(ScopeName::try_from("@foo").is_err()); + // Test invalid scope names with dots + assert!(ScopeName::try_from(".foo").is_err()); + assert!(ScopeName::try_from("foo.").is_err()); + assert!(ScopeName::try_from("foo..bar").is_err()); + assert!(ScopeName::try_from(".org").is_err()); + assert!(ScopeName::try_from("test.").is_err()); } #[test] diff --git a/frontend/utils/ids.ts b/frontend/utils/ids.ts index 134ef45f..49dffcd2 100644 --- a/frontend/utils/ids.ts +++ b/frontend/utils/ids.ts @@ -6,8 +6,14 @@ export const validateScopeName = (name: string) => { if (name.length < 3) { return "Name must be at least 3 characters."; } - if (!/^[a-zA-Z0-9-_]+$/.test(name)) { - return "Name can only contain letters, numbers, dashes, and underscores."; + if (!/^[a-z0-9-.]+$/.test(name)) { + return "Name can only contain lowercase letters, numbers, dashes, and dots."; + } + if (name.startsWith('.') || name.endsWith('.')) { + return "Name must not start or end with a dot."; + } + if (name.includes('..')) { + return "Name must not contain consecutive dots."; } return null; }; From 052ed237d446e25ffde1474c14835be9834ecc4d Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Fri, 24 Oct 2025 11:38:46 +0000 Subject: [PATCH 2/3] fix: Apply deno fmt formatting to ids.ts --- frontend/utils/ids.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/utils/ids.ts b/frontend/utils/ids.ts index 49dffcd2..36dc8328 100644 --- a/frontend/utils/ids.ts +++ b/frontend/utils/ids.ts @@ -9,10 +9,10 @@ export const validateScopeName = (name: string) => { if (!/^[a-z0-9-.]+$/.test(name)) { return "Name can only contain lowercase letters, numbers, dashes, and dots."; } - if (name.startsWith('.') || name.endsWith('.')) { + if (name.startsWith(".") || name.endsWith(".")) { return "Name must not start or end with a dot."; } - if (name.includes('..')) { + if (name.includes("..")) { return "Name must not contain consecutive dots."; } return null; From 2a0b300adcca042ed0a7e7bfc53f64eb9dcd07f0 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Fri, 24 Oct 2025 12:50:18 +0000 Subject: [PATCH 3/3] fix: Apply cargo fmt formatting to ids.rs --- api/src/ids.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/src/ids.rs b/api/src/ids.rs index 66fb307f..8dc9ef11 100644 --- a/api/src/ids.rs +++ b/api/src/ids.rs @@ -27,7 +27,9 @@ impl ScopeName { if !name .chars() // temp allow underscores - .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '.') + .all(|c| { + c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '.' + }) { return Err(ScopeNameValidateError::InvalidCharacters); }