Reference solution
The findings below are ordered by priority.
Findings
1. Unicode compatibility forms never reach the canonical representation
Location: src/username-normalization.ts:1-2
The function trims and lowercases the original spelling without applying NFKC. Compatibility-equivalent input such as full-width Latin characters is rejected instead of resolving to the same ASCII uniqueness key as its canonical spelling.
Apply NFKC before trimming and lowercasing, then validate the resulting value.
2. Canonical length is validated against the source spelling
Location: src/username-normalization.ts:4-6
The length check reads input.length, before trimming or compatibility normalization, and counts UTF-16 code units rather than Unicode code points. Surrounding whitespace can reject an otherwise valid username, while normalization can change the length that is actually stored.
After all normalization steps, count code points in the canonical value and enforce the inclusive 3-to-20 range there.
3. Hyphens are silently converted into underscores
Location: src/username-normalization.ts:8-12
The validation explicitly accepts hyphens and then rewrites them. The contract requires rejection, and the rewrite makes distinct-looking submissions such as team-one and team_one compete for the same key without telling the caller why.
Allow only letters, digits, and underscores in the canonical value and throw for a hyphen.
4. Lowercasing depends on the process locale
Location: src/username-normalization.ts:2
Calling toLocaleLowerCase without a fixed locale lets host configuration affect identity. The same input can therefore normalize differently across service instances, undermining uniqueness and sign-in lookup.
Use the specified locale-independent lowercase operation after NFKC.
Reasonable non-findings
- Trimming before validation is required; only its position after NFKC matters.
- A
RangeError is the documented failure type for invalid canonical values.
- Returning one string is appropriate because persistence and collision reporting belong to the caller.