Project
Changelog
What changed in each release. The newest is at the top.
#0.23.0
0.23.0Unreleased
This release changes what counts as a lookalike. Skeletons change, so recompute any you have stored.
#Changed
- The character maps are built from Unicode’s latest confusables.txt (2026-08-06) and from confusable-vision’s in-place measurements, in a new file,
src/confusable-maps.ts.CONFUSABLE_MAP_FULLhas 2,216 entries (was 1,425) andCONFUSABLE_MAP1,018 (was 613). - Unicode’s ASCII entries are used: 1 and | as l, 0 as o, m as rn.
skeleton("paypa1")equalsskeleton("paypal"), andskeleton("microsoft")is"rnicrosoft", as isskeleton("rnicrosoft"). Earlier versions dropped these, soareConfusable("rnicrosoft", "microsoft")was false. - Multi-letter targets are kept: ǁ as ll, Ⅱ as ll, lj as lj.
confusableDistance()treats one character standing for two (m for rn) as a lookalike substitution, not an edit. - Characters are looked up as typed, before lowercasing. A capital and its lowercase form can look like different letters: Greek Η looks like H, η does not look like h. Lowercasing first hid lookalike capitals such as Cherokee Ꭱ (R), which lowercases to ꭱ.
checkRisk()andconfusableDistance()now look characters up as typed, and validators receive the identifier as typed as a second argument, whichcreateHomoglyphValidator()also checks. - Names still compare without case:
skeleton("ADMIN")equalsskeleton("admin"). Unicode’s capital I as l is kept apart, inCONFUSABLE_MAP_CASED.areConfusable()checks both ways, so"paypaI"(capital I) matches"paypal": on a site that stores names in lowercase but shows them as typed, paypaI is a different name that looks the same. checkRisk()blocks only when a name looks like a protected one: its skeleton collides (with or without case), or it differs only by lookalikes, aspaypa1andpaypaIdo frompaypal(both 100), or it has lookalike, invisible or mixed-script characters. One letter changed, added, dropped or swapped that is not a lookalike warns at most (paypaxscores 69, one under the block threshold, where it scored 83), and a protected word with letters added or dropped at an end is not matched, sohelper,adminsandsettingare allowed where 0.22.0 blocked them.- A reserved name that is not also in
protectis matched by lookalikes only, not by a one-letter typo: withhelpreserved,hellis allowed andhеlp(Cyrillic е) blocks. checkRisk()looks for characters where NFKC and TR39 disagree (ſ, which NFKC stores as s and Unicode lists as f) in the name as typed; before, it looked after NFKC had rewritten them, so the signal never fired. Such a character is not a lookalike in the stored name, so it cannot take a close spelling to a block:microsoſt(storedmicrosost) warns at 69, where it scored 89.skeleton(),areConfusable()andconfusableDistance()use the map’s entries for the 20 characters that NFD splits before lookup (í, ḋ, Ά, ἰ, וֹ). Before, each became a letter and a mark that counted as a difference, soareConfusable("aḋmín", "admin")andareConfusable("mícrosoft", "microsoft")were false; both are now true.checkRisk()also puts back ŀ, which NFKC stores as l and a middle dot.- The CLI’s
calibrateandrecommendsweep thresholds ascheckRisk()applies them: a name that cannot block (a close spelling) never counts as a block at any threshold, and the warn threshold is always below the block threshold.namespace-guard riskscores the name as typed, sorisk paypaI --protect paypalblocks. - 394 characters Unicode does not map come from confusable-vision: set between neighbours in five common fonts at text size, and as alike there as accepted confusables such as 0/O and 1/l. Hebrew א, found alike to x in Arial at 16 px, is one.
CONFUSABLE_MAP, which the homoglyph validator uses to reject a name for containing a lookalike, takes only the strict tier of these; the broad and design tiers count when two names are compared. - The composability suite is now
nfkc-tr39-divergence-v2, with 34 vectors (was 31): the new confusables.txt adds ₀, 0 and 1, which NFKC reads as digits and Unicode’s list as o and l. The CI drift budget moves to 34 action flips. The vectors are derived with the runtime’s NFKC, and three of them (U+1CCDE, U+1CCF0, U+1CCF1) are Unicode 16 characters, so a runtime whose Unicode is older derives 31. - Profanity matching folds with single letters only (m stays m), so it matches as before.
- All confusable-vision data now comes from release 2026.09.26, which compares every letter and digit each of 322 fonts draws (release 2 compared only part of that):
CONFUSABLE_WEIGHTShas 2,300 pairs (was 322),FONT_SPECIFIC_WEIGHTS1,417 pairs in 166 fonts (was 130 in 106), andLLM_CONFUSABLE_MAP2,326 pairs. The weights keep pairs where one character is an ASCII letter or digit or the two are in different scripts; the release’s same-script pairs (Hangul jamo variants, Arabic positional forms) are left out. Some release 2 pairs are gone with the correct font faces: Hangul ᅵ and Han 丨 are alike in 1 of 6 text fonts, not all (Hangul ㅣ and 丨 are alike in all). LLM_CONFUSABLE_MAPrewrites a character as one letter: it leaves out Unicode’s ASCII entries and multi-letter targets, and reads the prototype rn as m.canonicalise()rewrites a word in full when it shows a sign of tampering: it mixes Latin with another script, has a Latin letter not in modern use (ɦ, ꞡ, ꝱ, by Unicode’s Identifier_Type) or a fullwidth or mathematical letter, has a non-ASCII capital inside a lowercase word, or has a listed lookalike scoring at leastthreshold. In such a word every listed lookalike is replaced whatever its score, and a letter built on a Latin letter (ŧ, İ, ɏ) is folded to it, in the case of the word around it. Before, it replaced only lookalikes scoring 0.7 or more and had no entry for ŧ or İ, so it undid little of a flooded document: on the Denial of Spend test contract, 0.22.0 left 1,485 of 1,532 lookalikes. Nowstrategy: "all"restores that contract byte for byte and the default leaves 9 short words. Words with no sign of tampering (Turkish İstanbul, Russian Москва) are left alone as before;strategy: "all"folds ŧ, İ and ö too. Athresholdyou set is also a floor, as before. This goes back on 0.22.0’s choice to leave Cyrillic п in a Latin word: п doesn’t pass for n at text size, but mixed into a Latin word it still costs tokens and hides the word from search, so canonicalise() now rewrites it there.isClean()istrueexactly whencanonicalise()with the same options changes nothing, andscan()reports every rewrite; a letter folded to its base letter hassource: "fold".- The homepage playground kept its own copy of the maps, weights and scoring, and its weights were still the March 2026 measurements. It now runs the built library (
docs/lib/, fromnpm run build:docs-lib), so it shows what this release does.docs/data/confusable-weights.global.jsis regenerated from release 2026.09.26 for anyone loading it directly, and a pair found both within one font and across fonts keeps the stronger score.
#Added
MEASURED_CONFUSABLES: each measured lookalike’s letter, tier (strict, broad or design) and the fonts and sizes where it holds, such as 1 as l in Times New Roman at 16 px.CONFUSABLES_DATE: the confusables.txt version the maps come from.preserveCaseoption forskeleton(): look characters up with their case, for names shown as typed.leetspeakrisk option (inriskconfig,checkRisk()andenforceRisk();--leetspeakin the CLI), off by default: leetspeak swaps count as lookalikes, sop4ypal,adm1nand4dm1nblock. It uses the profanity matcher’s aggressive table (4 for a, 1 for i or l, 5 or $ for s, 8 for b, 9 or 6 for g). Without it they warn at most, andattack-gen --mode evasioncounts them as bypasses.RiskMatch.evidence(exact,lookalike,lookalike-extensionortypo) andRiskCheckResult.canBlock, false when nothing in the name is visual and the score is held below the block threshold.src/latin-fold.ts(generated bynpm run build:latin-foldfrom the Unicode Character Database): 797 Latin letters built on an ASCII letter, and the 817 Latin letters not in modern use, forcanonicalise().allowlistoption forcreateProfanityValidator(): words that contain a listed word but are fine, such as names and places. A listed word found only inside one doesn’t count, soallowlist: ["scunthorpe"]letsscunthorpe-fcthrough and still refusesxcuntx; an entry written with separators (chorlton-cum-hardy) lets through only that exact name.PROFANITY_ALLOWLIST_ENinnamespace-guard/profanity-enis the English list’s, whichcreateEnglishProfanityValidator()uses.scripts/measure-profanity.mjsderives the English allowlist from openly licensed data (ONS baby names, US census surnames, GeoNames places, Webster’s dictionary; see THIRD-PARTY-NOTICES.md), screening out names, places and words that look like a slur or read as crude.
#Fixed
- Adapters selected a column named after
scopeKey, although the guard compares the scope’s value with the id: with scope keys such asuserIdandorgId, which two tables sharing a namespace need, the query asked for a column that doesn’t exist. They now select only the id column. - The Mongoose adapter looks up
_idwhenidColumnisn’t set, but the guard readid, so nobody could keep their own name. It now returns_idasidtoo. - With
cacheon, a lookup that failed was cached for the wholettl, so the same error came back until it expired. Failed lookups are no longer kept. - With
caseInsensitiveon, the Drizzle, Kysely, TypeORM and MikroORM adapters passed_and%to ILIKE as wildcards. They’re escaped now; this matters only if your pattern allows them. org-sluganddeveloper-idrefused long names with “Use 2-30 lowercase letters…”, though they allow 40 and 50. Each profile now has its own message.isLikelyUniqueViolationError()counted any SQLite constraint failure, NOT NULL and CHECK included, as a taken name. It now needs a unique one.assertAvailable()looked up suggestions and threw them away; it no longer looks them up.- Validators run on suggestions now get the candidate as typed, as they do in
check(). LlmConfusableMapEntryis exported.- With default settings the English profanity list refused 18,974 of the 234,456 words in /usr/share/dict/words, 3,327 of 38,970 first names given in England and Wales since 1996, 9,047 of 162,254 US surnames and 4,290 of 43,818 UK place names (
hello,class,grape,wang,scunthorpe); it now refuses 890, 11, 259 and 62, and still refuses all of its words’ leetspeak, lookalike, separated and repeated-letter disguises in the tests (5h1t,ѕhit,s-h-i-t) andshitposter. The matcher reads symbols in an entry as letters (sh!+was looked for assh), counts letters rather than spaces forminSubstringLength, matches short and spaced entries and 122 letter sequences common inside English words (anal,rape) only as whole words, and reads a tripled letter as a repeat; the list drops 78 entries that are names or places (wang,pula) and allows 2,278 names, places and words that contain a listed word (scunthorpe,dickson), leaving out those that look like a slur or read as crude (niggli,bitchfield). - Reserved names are normalised like any name, so
Adminin the list reservesadmin; before, the list was compared as written andAdminmatched nothing. - The CLI exited before a pipe had read all its output, so
--jsonoutput over 64 KB was cut short. It now sets the exit code and lets the output finish. areConfusable()withweightsreturnedtruewhen any character of one string and any of the other were a measured pair, soareConfusable("Iowa", "lima", { weights: CONFUSABLE_WEIGHTS })wastrue. It now lines the two up character by character: each position must share a skeleton or be a measured pair, with m standing for rn as before.context: "identifier"or"domain"checked only the character a weight is listed under, so a pair found the other way round took that character’s flags:areConfusable("ㅣ", "丨", { weights, context: "domain" })wastruethough Hangul ㅣ can’t appear in a domain name. Both characters must now be allowed in the context.- The CLI’s
calibrateandrecommendbroke ties towards the highest thresholds, so on data where every attack scores 100 they suggested blocking only at 100. Among thresholds that cost the same they now take the middle of the range, warn first and then block above it. driftlisted each built-in row’s target twice (targets [o,o]); a target named in bothtargetandprotectis listed once.- Every CLI command printed the same help.
namespace-guard <command> --helpandnamespace-guard help <command>now print that command’s usage, options and examples.
#Measured against 0.22
On confusable-vision’s in-place measurements (3,048 candidate pairs, each set in a word), areConfusable() now catches 98% of the strict-tier lookalikes (0.22: 48%), 96% of the broad tier (40%) and 96% of the design tier (32%). Of the pairs measured as not alike, it flags 29% (18%): nearly all of the difference is Unicode’s own entries, such as fullwidth digits, which NFKC folds anyway.
#0.22.0
0.22.02026-09-24npm
#Changed
FONT_SPECIFIC_WEIGHTSnow comes from confusable-vision release 2026.09.24: 130 pairs across 106 fonts, each scored by how close it is in that font (1 identical, 0 at the alike threshold). The previous file dated from the February SSIM measurements, which were blind to size.LLM_CONFUSABLE_MAPis rebuilt from the release 2 weights (1,425 TR39 + 166 novel entries). Pairs release 2 finds unlike at running-text size, such as Cyrillic п and Latin n, are no longer rewritten.
#Fixed
CONFUSABLE_WEIGHTSkept the weaker score for the 50 pairs found alike both within one font and across fonts (Tamil zero ௦ and o shipped at 0.41 instead of 1). It now keeps the stronger, and ships 322 distinct pairs; the 0.21.0 note’s 372 counted both findings.
#0.21.0
0.21.02026-09-24npm
#Changed
CONFUSABLE_WEIGHTSnow comes from confusable-vision release 2026.09.24 (372 pairs). The 0.20.0 weights measured glyphs without their size or baseline, so they flagged pairs readers tell apart (c/o, D/O and case pairs), and they had dropped the earlier same-script pairs (Latin small capitals, dotless ı and others). Each bundled pair was found alike at the size and baseline position the two glyphs have in running text, within one font or across fonts; the weights are now the share of text fonts (or font combinations) where it holds.isDomainSpoof(): a pair TR39 maps keeps a similarity of at least 0.5, so a measurement that finds it alike in only some fonts does not undo Unicode’s own mapping.
#Added
ignoreDiacriticsoption forskeleton()andareConfusable(): removes combining diacritical marks before comparing, soạmatchesaandẹmatchese. Off by default, as TR39 keeps them.
#0.20.0
0.20.02026-03-02npm
#Added
isDomainSpoof(label, target, options?)checks whether a domain label is a registrable lookalike of a target label. It flags only labels written in one script (digits and hyphens aside), since registrars reject mixed-script labels, and only when every character that differs from the target is a confusable of the target’s character at the same position.isDomainSpoof()returns aDomainSpoofResultwithspoofand, when the label matches,script,danger(the average similarity of the substitutions) andsubstitutions(DomainSpoofSubstitution). Options (DomainSpoofOptions):map(defaultCONFUSABLE_MAP_FULL),weights,minDanger(default0.5) andallowlistfor known non-Latin labels.
#Changed
- Breaking:
ssimScoreis renamedvisualScoreonScanFindingand onLLM_CONFUSABLE_MAPentries (LlmConfusableMapEntry), andssimis renamedvisualScoreonCrossScriptRiskResult.crossScriptPairs. The scores inLLM_CONFUSABLE_MAPare unchanged. CONFUSABLE_WEIGHTSnow comes from confusable-vision’s second weights file: 4,174 pairs measured across 245 fonts by raycasting glyph outlines (RaySpace), in place of 1,397 pairs scored by SSIM across 230 fonts. 3,525 of the pairs are between two non-Latin scripts, every pair has adangerof at least 0.5, and 1,292 of the earlier pairs are no longer included.- Pairs between two non-Latin scripts in
CONFUSABLE_WEIGHTSnow carryxidContinueandidnaPvalidflags. In 0.18 and 0.19 they had neither, socontext: "identifier"andcontext: "domain"left them out. - README, reference and playground describe the RaySpace measurement and
isDomainSpoof().
#0.19.1
0.19.12026-02-28npm
#Changed
- Documentation only: the README’s opening line links to the Denial of Spend write-up.
#0.19.0
0.19.02026-02-28npm
#Added
namespace-guard/font-specific-weightssubpath export withFONT_SPECIFIC_WEIGHTS: aConfusableWeightsmap for each of 74 macOS system fonts, keyed by font name, holding only the pairs that score 0.7 or more by SSIM in that font (6 to 264 pairs per font). A font’s map can be passed asweightsin place ofCONFUSABLE_WEIGHTS.
#Changed
- README describes the PRI #540 findings as submitted to Unicode public review, with a link to the published feedback, rather than as accepted.
#0.18.0
0.18.02026-02-27npm
#Added
detectCrossScriptRisk(identifier, options?)andCrossScriptRiskResult: lists the scripts in an identifier and, givenweights, the pairs of characters from different scripts that have a measured weight.riskLevelis"none","low", or"high"when a pair scores 0.8 or more or there are three or more pairs.weightsandcontextoptions forareConfusable()(AreConfusableOptions). Withweights, two strings also count as confusable when a character in one has a measured weight against a character in the other. Withoutweightsit compares skeletons only, as before.
#Changed
CONFUSABLE_WEIGHTShas 1,397 pairs (was 903). The 494 new pairs are between characters of different scripts, most of them between two non-Latin scripts such as Cyrillic and Greek or Han and Hangul.- Hangul, Georgian and Thai are now told apart from each other and from other unlisted scripts, which changes when
confusableDistance()treats a substitution as cross-script. canonicalise(),scan()andisClean()return early for ASCII text and for text with no character inLLM_CONFUSABLE_MAP.- Package description, README and reference docs lead with cross-script detection and Denial of Spend (the extra token cost of confusable characters in LLM input).
#0.17.1
0.17.12026-02-26npm
#Added
maxSizeRatiooption forcanonicalise(),scan()andisClean()(default3.0). A novel pair is skipped when the source character is more than this many times wider or taller than its Latin target. TR39 pairs and pairs without measured ratios are not affected;Infinityturns the check off.widthRatioandheightRatioonLLM_CONFUSABLE_MAPentries (LlmConfusableMapEntry), from confusable-vision’s size measurements. 2,133 of the 2,218 pairs are measured; the rest arenull.
#0.17.0
0.17.02026-02-26npm
#Added
- LLM preprocessing APIs:
canonicalise(text, options?)scan(text, options?)isClean(text, options?)
- Exported LLM preprocessing types:
CanonicaliseOptionsScanOptionsScanFindingScanResult
- Generated static lookup dataset for preprocessing:
src/llm-confusable-map.ts(LLM_CONFUSABLE_MAP+ metadata/count exports)scripts/generate-llm-confusable-map.jsbuild:llm-confusable-mapnpm script
- Test coverage for LLM preprocessing behavior and performance sanity:
tests/llm-preprocessing.test.ts
#Changed
- README now includes a dedicated “LLM Pipeline Preprocessing” section with usage and pipeline diagram
- Reference docs now document LLM preprocessing behavior/options and API contracts
- Playground now surfaces LLM preprocessing in “What It Does” and “Advanced API primitives”
#0.16.1
0.16.12026-02-25npm
#Changed
- Documentation only: README and reference docs cover the
weightsandcontextoptions ofconfusableDistance()and thenamespace-guard/confusable-weightssubpath, and the playground can score with the measured weights (built by the newbuild:weights-globalscript).
#0.16.0
0.16.02026-02-25npm
#Added
ConfusableWeightandConfusableWeightstypes for measured visual similarity dataweightsoption inConfusableDistanceOptionsto use measured SSIM-based costs instead of hardcoded 0.35contextoption inConfusableDistanceOptionsfor deployment-specific filtering ('identifier','domain','all')"visual-weight"reason inConfusableDistanceStepfor novel pairs recognized via the weight graphnamespace-guard/confusable-weightssubpath export with 903 scored pairs (110 TR39 + 793 novel discoveries from confusable-vision)scripts/generate-confusable-weights.jsto regenerate weights data from confusable-vision output
#Changed
buildSubstitutionStepuses measured cost from weights when available (falls back to hardcoded 0.35 when not)- Novel confusable pairs not in TR39 map can now be recognized via the weight graph instead of defaulting to cost 1
#0.15.1
0.15.12026-02-25npm
#Added
CODE_OF_CONDUCT.mdusing Contributor Covenant v2.1- Concise
CONTRIBUTING.mdwith practical contribution guidance - Contributor sign-off line in
CONTRIBUTING.md
#Changed
- Release bump to
0.15.1
#0.15.0
0.15.02026-02-25npm
#Added
- Composability suite aliases:
COMPOSABILITY_VECTOR_SUITECOMPOSABILITY_VECTORSCOMPOSABILITY_VECTORS_COUNT
namespace-guard/composability-vectorsexport subpathcreateInvisibleCharacterValidator()andInvisibleCharacterValidatorOptions, including opt-inrejectCombiningMarks- Reproducible composability artifact pipeline:
scripts/generate-composability-vectors.jsdocs/data/composability-vectors.jsondocs/data/composability-vectors.SOURCE.md
- Reproducible confusable benchmark corpus:
scripts/generate-confusable-bench.jsdocs/data/confusable-bench.v1.jsondocs/data/confusable-bench.v1.SOURCE.md
- Dataset integrity tests:
tests/composability-vectors.test.tstests/confusable-bench.test.ts
#Changed
- CLI drift/recommend built-in baseline dataset label from
builtin:nfkc-tr39-divergence-vectorstobuiltin:composability-vectors tsupentrypoints now includesrc/composability-vectors.ts- Added build scripts:
build:composability-databuild:confusable-bench
- Documentation refresh across:
README.mddocs/reference.mddocs/index.html(playground/workflow/research/advanced API sections)
#0.14.0
0.14.02026-02-25
Not published to npm; these changes first shipped in 0.15.0.
#Added
guard.claim(identifier, write, options?)for race-safe claim/write workflowsisLikelyUniqueViolationError(error)helper for duplicate-key detection across common stacks- CLI
audit-canonicalcommand for preflight canonical collision/mismatch analysis on exported datasets
#Changed
- README now includes per-adapter canonical uniqueness migration guidance and operational rollout notes
- Playground/docs workflow sections updated for calibration/recommendation/drift operations and moderation coverage
#0.13.0
0.13.02026-02-25
Not published to npm; these changes first shipped in 0.15.0.
#Added
- Curated profanity subpath export:
namespace-guard/profanity-en createEnglishProfanityValidator()helperPROFANITY_WORDS_EN,PROFANITY_WORDS_EN_COUNT,PROFANITY_WORDS_EN_SOURCE,PROFANITY_WORDS_EN_LICENSEexports- Profanity dataset provenance docs and generated preload asset:
docs/data/profanity-words.SOURCE.mddocs/data/profanity-words.global.jsscripts/generate-profanity-global.js
#Changed
- Zero-dependency moderation story now supports:
- curated built-in English list via subpath
- bring-your-own predicate validator path
- Release bump to
0.13.0
#0.12.0
0.12.02026-02-24
Not published to npm; these changes first shipped in 0.15.0.
#Added
- CLI
recommendcommand that combines calibration + drift baseline analysis and outputs ready-to-paste risk/CI guidance - CI drift gate tooling:
scripts/drift-gate.js- GitHub Actions workflow
.github/workflows/drift-gate.yml
ci:drift-gatenpm script
#Changed
- Release bump to
0.12.0
#0.11.1
0.11.12026-02-23npm
#Added
- Additional tests in
tests/index.test.ts - Expanded npm package keywords/topics metadata
#0.11.0
0.11.02026-02-22npm
#Added
validateFormatOnly()method: validates format and purely-numeric restriction without checking reserved names or querying the database, for instant client-side feedbackCheckManyOptionsexported type:checkMany()now accepts{ skipSuggestions?: boolean }(default:true) to opt in to suggestions per batch call
#Changed
- Removed unnecessary type casts in config message parsing
- Expanded API reference in README for
validateFormat,clearCache,cacheStats,normalize - Drizzle adapter docs now note
db.query(relational query API) requirement - Raw SQL adapter docs now include MySQL2 and better-sqlite3 wrapper examples
#0.10.0
0.10.02026-02-22npm
#Added
skeleton()function: TR39 Section 4 skeleton algorithm (NFD + ignorable removal + confusable map + NFD) for confusable string comparison - the same algorithm used by ICU SpoofChecker, Chromium, and the Rust compilerareConfusable()function: returns true if two strings produce the same skeletonSkeletonOptionsexported type for configuring the confusable map used byskeleton()andareConfusable()
#0.9.0
0.9.02026-02-22
Not published to npm; these changes first shipped in 0.10.0.
#Added
CONFUSABLE_MAP_FULLexport: complete TR39 confusable mapping (~1,400 entries) with no NFKC filtering, for use in pipelines that don’t run NFKC normalization before confusable detection (TR39 skeleton uses NFD, Chromium uses NFD, Rust uses NFC, django-registration uses no normalization)scripts/generate-confusables.tsnow outputs bothCONFUSABLE_MAP(NFKC-filtered, 613 entries) andCONFUSABLE_MAP_FULL(unfiltered, ~1,400 entries)
#0.8.2
0.8.22026-02-22npm
#Fixed
- Escape regex metacharacters (
\,],^,-) when building the confusable character class increateHomoglyphValidator- prevents regex breakage ifadditionalMappingscontain these characters - Same fix applied to the playground’s inline validator
- CLI: guard against undefined
Poolexport frompgmodule - Raw SQL adapter: validate table/column identifiers against
[a-zA-Z_][a-zA-Z0-9_]*to prevent SQL injection via malformed config
#Added
cache.maxSizeoption to configure maximum cached entries before LRU eviction (default: 1000)
#Changed
scramblestrategy no longer skips identical adjacent character swaps (dedup Set already handles duplicates)similarstrategy JSDoc now documents QWERTY keyboard layout assumption
#0.8.1
0.8.12026-02-20npm
#Fixed
- Removed 31 NFKC-conflict entries from
CONFUSABLE_MAP(644 → 613 pairs) - these encoded wrong mappings in any pipeline that runs NFKC normalization first (e.g., Long Sſmapped tofby TR39 but correctly tosby NFKC; Mathematical Bold I𝐈mapped tolby TR39 but correctly toiby NFKC) scripts/generate-confusables.tsnow automatically detects and excludes NFKC-conflict entries
#Changed
- Expanded
rejectMixedScriptregex from 7 script ranges to 19+ (added Hebrew, Arabic, Indic, Thai, Myanmar, Ethiopic, Runic, Khmer, Coptic, Tifinagh, Lisu, Bamum) - README now documents the three-stage anti-spoofing pipeline and NFKC-aware filtering rationale
- Playground now includes an anti-spoofing pipeline explainer section
#0.8.0
0.8.02026-02-20
Not published to npm; these changes first shipped in 0.8.1.
#Added
- Full Unicode TR39 confusables.txt coverage -
CONFUSABLE_MAPnow contains 613 character pairs (up from 30), covering Cyrillic, Greek, Armenian, Cherokee, IPA, Coptic, Lisu, Canadian Syllabics, Georgian, Latin small capitals, and 20+ other scripts scripts/generate-confusables.ts- reproducible build script that downloads the official Unicode confusables.txt, filters to Latin-target single-character mappings, excludes NFKC-redundant entries, and adds supplemental Latin small capitals- Expanded mixed-script detection -
rejectMixedScriptnow covers all scripts with confusable entries (Hebrew, Arabic, Indic, Thai, Myanmar, Georgian, Ethiopic, Cherokee, Canadian Syllabics, Runic, Khmer, Coptic, Tifinagh, Lisu, Bamum, and more) in addition to Cyrillic and Greek
#Changed
CONFUSABLE_MAPis now generated from the Unicode Consortium’s authoritative source rather than hand-curated- All
CONFUSABLE_MAPtargets are now lowercase (uppercase Cyrillic targets like"A"→"a"are lowercased to match the normalize pipeline)
#0.7.0
0.7.02026-02-20npm
#Added
- NFKC Unicode normalization in
normalize()- collapses full-width characters, ligatures, superscripts, and other compatibility forms to canonical equivalents (on by default, opt out withnormalizeUnicode: false) createHomoglyphValidator()- detects Cyrillic and Greek characters that visually mimic Latin letters (e.g., Cyrillic “а” in “аdmin”)CONFUSABLE_MAPexport - ~30 Cyrillic-to-Latin and Greek-to-Latin confusable character pairsrejectMixedScriptoption for homoglyph validator - also rejects strings mixing Latin + Cyrillic/Greek scriptsallowPurelyNumericconfig option - reject purely numeric identifiers like “123” (default: allowed)messages.purelyNumericfor custom rejection message
#Changed
normalize()now accepts an optionaloptionsparameter ({ unicode?: boolean })- Playground updated with homoglyph detection toggle, purely-numeric toggle, and confusable example pills
#0.6.1
0.6.12026-02-20npm
#Changed
- Documentation only: the README describes the suggestion pipeline as progressive batched rather than three-phase.
#0.6.0
0.6.02026-02-20npm
#Added
"similar"suggestion strategy - generates cognitively close alternatives using edit-distance-1 mutations (deletions, keyboard-adjacent substitutions, prefix/suffix additions)- LRU cache eviction replaces FIFO for better hit rates on frequently checked names
#Changed
- Suggestion pipeline now uses progressive batched processing - validates and DB-checks in parallel batches of
maxinstead of validating all then checking sequentially (up to 5-6x latency improvement) - Pre-compiled regex for profanity substring matching - O(identifier length) instead of O(words x length)
- Set-based deduplication in all strategy factories - O(n) instead of O(n²)
- Binary search in
extractMaxLength- 12x faster pattern initialization
#0.5.0
0.5.02026-02-20
Not published to npm; these changes first shipped in 0.6.0.
#Added
- Pluggable suggestion strategies:
"sequential","random-digits","suffix-words","short-random","scramble" SuggestStrategyNameexported type- Strategy composition via arrays (e.g.,
strategy: ["random-digits", "suffix-words"]) - Custom strategy functions via
strategy: (id) => string[] - Optimized three-phase suggestion pipeline (sync format/reserved filter → async validators → DB checks)
#Changed
- Default suggestion strategy changed from sequential-only to
["sequential", "random-digits"] - Suggestions now skip reserved names and format-invalid candidates without DB calls
generatecallback is now deprecated in favor ofstrategy(still works for backwards compatibility)
#0.4.0
0.4.02026-02-20npm
#Added
createProfanityValidator()- convenience factory for blocking offensive names (bring your own word list)cacheStats()method - returns{ size, hits, misses }for cache performance monitoring- Smarter default suggestions: interleaves hyphenated (
sarah-1) and compact (sarah1) variants, with truncation for identifiers near the max length - JSDoc on all public types and methods (visible in editor hover tooltips and
.d.tsoutput) - Bundle size badge in README
#Changed
- Default suggestion output order changed from
["sarah-1", "sarah-2", "sarah-3"]to["sarah-1", "sarah1", "sarah-2"](interleaved compact variants) clearCache()now also resets hit/miss counters
#0.3.0
0.3.02026-02-20
Not published to npm; these changes first shipped in 0.4.0.
#Added
- TypeORM adapter (
namespace-guard/adapters/typeorm) - MikroORM adapter (
namespace-guard/adapters/mikro-orm) - Sequelize adapter (
namespace-guard/adapters/sequelize) - Mongoose adapter (
namespace-guard/adapters/mongoose)
#Fixed
- Knex adapter now uses
??identifier binding for cross-dialect portability - Removed phantom
whereRawfrom Kysely type definition - Raw SQL adapter docs now correctly state PostgreSQL-only
$1placeholder syntax
#0.2.0
0.2.02026-02-20npm
#Added
- Case-insensitive database matching (
caseInsensitiveconfig option) - In-memory TTL cache for adapter lookups (
cacheconfig option) clearCache()method- Kysely adapter (
namespace-guard/adapters/kysely) - Knex adapter (
namespace-guard/adapters/knex) - Framework integration examples in README (Next.js, Express, tRPC)
- OG social preview image for playground
#0.1.2
0.1.22026-02-19npm
#Added
- Sponsor and support links in README and playground footer
#0.1.1
0.1.12026-02-19
Not published to npm; these changes first shipped in 0.1.2.
#Added
- SEO and Open Graph meta tags for playground page
- npm and repository metadata in
package.json
#0.1.0
0.1.02026-02-19npm
#Added
- Core
createNamespaceGuardfactory withcheck,checkMany,assertAvailable,validateFormat normalize()utility (trim, lowercase, strip@)- Reserved name blocking with categorized records and per-category messages
- Multi-source collision detection with parallel database queries
- Ownership scoping to prevent false collisions on own records
- Async validator hooks (
validatorsconfig) - Conflict resolution suggestions (
suggestconfig) - Prisma adapter (
namespace-guard/adapters/prisma) - Drizzle adapter (
namespace-guard/adapters/drizzle) - Raw SQL adapter (
namespace-guard/adapters/raw) - CLI (
npx namespace-guard check <slug>) - Interactive playground page (GitHub Pages)