namespace-guarddocs

Reference

Functions

Everything namespace-guard exports that you call directly, other than the guard’s own methods. Each function is imported from namespace-guard unless its section says otherwise. The guard’s methods are on The guard, and the maps, weights and lists on Data and maps.

FunctionWhat it does
createNamespaceGuard()makes a guard
createNamespaceGuardWithProfile()makes a guard from a profile
normalize()a name’s canonical form
skeleton()what a string looks like, with each lookalike replaced
areConfusable()whether two strings look alike
confusableDistance()how far apart two strings look, step by step
detectCrossScriptRisk()lookalike pairs between scripts inside one string
isDomainSpoof()whether a domain label is a registrable lookalike of another
canonicalise()text with lookalikes put back to Latin letters
scan()the lookalikes in a text, and a risk level
isClean()whether canonicalise() would change anything
createHomoglyphValidator()a validator that refuses lookalike characters
createInvisibleCharacterValidator()a validator that refuses invisible and direction characters
createProfanityValidator()a validator that refuses words from your list
createEnglishProfanityValidator()the same, with a curated English list
createPredicateValidator()a validator from any yes-or-no test
isLikelyUniqueViolationError()whether a database error means a duplicate key
deriveNfkcTr39DivergenceVectors()the characters where NFKC and a map disagree

#createNamespaceGuard() and createNamespaceGuardWithProfile()

createNamespaceGuard(config, adapter)

createNamespaceGuardWithProfile(profile, config, adapter)

Make a guard. The config is covered in Configuration, the adapter in Adapters, and the guard’s methods in The guard.

#normalize()

normalize(raw, options?): string

A name’s canonical form: trimmed, NFKC-normalised, lowercase, with any leading @ removed. It’s the form the guard checks and the form to store in a canonical column.

OptionDefaultWhat it does
unicodetrueapply NFKC; false only trims, lowercases and drops the @
TypeScript
normalize("  @Sarah ");  // → "sarah"
normalize("ACME-Corp");  // → "acme-corp"
normalize("@@team");  // → "team"
normalize("hello");  // → "hello"
normalize("hello", { unicode: false });  // → "hello"

Use { unicode: false } only if the guard has normalizeUnicode: false, so your stored names match what it checks. Normalisation covers what NFKC folds.

#skeleton()

skeleton(input, options?): string

What a string looks like, following Unicode’s skeleton algorithm (UTS #39): NFD, remove invisible characters, replace each character with the letters it passes for, NFD again, lowercase. Two strings with the same skeleton look alike. A skeleton is for comparing, not for showing anyone.

OptionDefaultWhat it does
mapCONFUSABLE_MAP_FULLthe character map; use CONFUSABLE_MAP if your text has been through NFKC
preserveCasefalsetreat the string as shown with its case, so capital I counts as l
ignoreDiacriticsfalsealso remove accents and other combining diacritics, so ạ matches a
TypeScript
skeleton("pаypal");  // → "paypal" (Cyrillic а)
skeleton("pay\u200Bpal");  // → "paypal" (a zero-width space)
skeleton("microsoft");  // → "rnicrosoft" (m passes for rn)
skeleton("paypaI");  // → "paypai"
skeleton("paypaI", { preserveCase: true });  // → "paypal"
skeleton("ạdmin", { ignoreDiacritics: true });  // → "adrnin"

Comparing names explains skeletons, case and the maps.

#areConfusable()

areConfusable(a, b, options?): boolean

Whether two strings look alike: true if their skeletons match, compared without case or with it. With weights, also true if the two line up character by character, each position holding the same letter, a listed lookalike or a measured pair, in order.

OptionDefaultWhat it does
map, preserveCase, ignoreDiacriticsas for skeleton()passed to the skeletons
weightsnonemeasured pairs, such as CONFUSABLE_WEIGHTS from namespace-guard/confusable-weights
context"all"which measured pairs count: "identifier" (both characters allowed in identifiers), "domain" (both allowed in domain names) or "all"
TypeScript
import { CONFUSABLE_WEIGHTS } from "namespace-guard/confusable-weights";

areConfusable("rnicrosoft", "microsoft");  // → true
areConfusable("paypaI", "paypal");  // → true
areConfusable("gitlab", "github");  // → false
areConfusable("ㅣ", "丨");  // → false (Hangul and Han, not in Unicode's list)
areConfusable("ㅣ", "丨", { weights: CONFUSABLE_WEIGHTS });  // → true
areConfusable("Iowa", "lima", { weights: CONFUSABLE_WEIGHTS });  // → false

See Comparing names.

#confusableDistance()

confusableDistance(a, b, options?): ConfusableDistanceResult

A weighted edit distance from a to b, in which swapping a character for its lookalike costs less than an ordinary edit, with the path of steps that gives it. The guard’s checkRisk() scores with it.

OptionDefaultWhat it does
mapCONFUSABLE_MAP_FULLthe character map
weightsnonemeasured pairs: a lookalike costs its measured cost instead of 0.35, and measured pairs Unicode doesn’t list count too
context"all"which measured pairs count, as for areConfusable()
FieldWhat it is
distancethe total cost of the path; lower is more alike
maxDistancethe longer string’s length, used to scale similarity
similarity1 - distance / maxDistance, from 0 to 1
skeletonEqualwhether the two share a skeleton
normalizedEqualwhether they’re equal after NFKC and lowercasing
chainDepthhow many steps aren’t matches
crossScriptCountlookalike swaps between two scripts
ignorableCountinvisible characters added or removed
divergenceCountswaps involving a character NFKC and Unicode’s list read differently
stepsthe path: one { op, from, to, fromIndex, toIndex, cost } per step, with prototype, crossScript, divergence and reason where they apply
TypeScript
const result = confusableDistance("paypa1", "paypal");
result.distance;  // → 0.35
result.similarity;  // → 0.942
result.skeletonEqual;  // → true
result.steps[5];  // → { op: "confusable-substitution", from: "1", to: "l", cost: 0.35, prototype: "l" }

confusableDistance("gitlab", "github").distance;  // → 2 (two ordinary letters changed)

Reading the steps explains each kind of step and what it costs.

#detectCrossScriptRisk()

detectCrossScriptRisk(identifier, options?): CrossScriptRiskResult

Looks inside one string for letters from different scripts that look alike, using measured weights. It returns the scripts it found, the lookalike pairs between them, and a risk level.

OptionDefaultWhat it does
weightsnonemeasured pairs. Without them, no pairs are found and the level is always "none"
FieldWhat it is
scriptsthe scripts of the letters, sorted, such as ["han", "hangul"]
crossScriptPairseach lookalike pair: { a: { char, script }, b: { char, script }, visualScore }
riskLevel"none" with no pairs, "high" if a pair scores 0.8 or more or there are three or more pairs, otherwise "low"
TypeScript
import { CONFUSABLE_WEIGHTS } from "namespace-guard/confusable-weights";

detectCrossScriptRisk("hello");  // → { scripts: ["latin"], crossScriptPairs: [], riskLevel: "none" }
detectCrossScriptRisk("ㅣ丨", { weights: CONFUSABLE_WEIGHTS });  // → { scripts: ["han", "hangul"], riskLevel: "high" }
detectCrossScriptRisk("ㅣ丨", { weights: CONFUSABLE_WEIGHTS }).crossScriptPairs[0];  // → { a: { char: "ㅣ", script: "hangul" }, b: { char: "丨", script: "han" }, visualScore: 1 }

It recognises Latin, Cyrillic, Greek, Armenian, Hebrew, Arabic, Devanagari, Han, Hiragana, Katakana, Hangul, Georgian and Thai; letters of other scripts, digits and punctuation are skipped. See Two non-Latin scripts.

#isDomainSpoof()

isDomainSpoof(label, target, options?): DomainSpoofResult

Whether a domain label, such as the раураӏ in раураӏ.com, is a lookalike of your label that someone could register: written wholly in one script other than the target’s, letter for letter.

OptionDefaultWhat it does
weightsnonemeasured pairs, to score each swap by how alike it looks
minDanger0.5the lowest average similarity that counts as a spoof
allowlistnonelabels you know are genuine
mapCONFUSABLE_MAP_FULLthe character map
TypeScript
import { CONFUSABLE_WEIGHTS } from "namespace-guard/confusable-weights";

isDomainSpoof("раураӏ", "paypal", { weights: CONFUSABLE_WEIGHTS });  // → { spoof: true, script: "cyrillic", danger: 0.781 }
isDomainSpoof("рaypal", "paypal", { weights: CONFUSABLE_WEIGHTS });  // → { spoof: false } (mixed scripts can't be registered)

Domain names covers the result, the threshold and the allowlist.

#canonicalise()

canonicalise(text, options?): string

Puts lookalike letters back to the Latin letters they pass for, in words that show a sign of tampering, before text reaches a language model. Words in other scripts with no such sign are left alone.

OptionDefaultWhat it does
strategy"mixed""mixed" rewrites words that show tampering; "all" rewrites every word
threshold0.7the score at which a lookalike alone marks a word as tampered; set it, and nothing scoring lower is replaced
includeNoveltrueuse pairs confusable-vision measured, as well as Unicode’s list
scriptsallonly replace characters from these scripts, such as ["Cyrillic"]
maxSizeRatio3skip measured pairs whose sizes differ by more than this
TypeScript
canonicalise("The seller аssumes аll liаbility.");  // → "The seller assumes all liability."
canonicalise("Москва is the capital");  // → "Москва is the capital"
canonicalise("поп-refundable", { strategy: "all" });  // → "non-refundable"

Text for LLMs explains what counts as tampering.

#scan()

scan(text, options?): ScanResult

Reports the lookalike characters in a text: each character, the letter it stands for, the word it’s in, and a risk level for the whole text. It lists everything canonicalise() would change, and also lookalikes in words it would leave alone, such as the letters of a Russian word, which on their own make the risk level low. It takes canonicalise()’s options and riskTerms, words that raise the risk level when they’re targeted (legal and financial terms by default).

TypeScript
const report = scan("The seller аssumes аll liаbility.");
report.count;  // → 3
report.summary.riskLevel;  // → "high"
report.findings[0];  // → { char: "а", codepoint: "U+0430", script: "Cyrillic", latinEquivalent: "a", source: "tr39", word: "аssumes", mixedScript: true }

The result has hasConfusables, count, findings and a summary with distinctChars, wordsAffected, scriptsDetected and riskLevel ("none", "low", "medium" or "high"). See Check before you pay.

#isClean()

isClean(text, options?): boolean

true exactly when canonicalise() with the same options would leave the text unchanged. It stops at the first word it would change, so it’s cheap enough to run on everything.

TypeScript
isClean("The seller assumes all liability.");  // → true
isClean("The seller аssumes all liability.");  // → false
isClean("Москва is the capital");  // → true

#createHomoglyphValidator()

createHomoglyphValidator(options?): NamespaceValidator

A validator that refuses a name containing any character from CONFUSABLE_MAP, the lookalikes that survive NFKC. It checks the name as typed as well as the canonical form.

OptionDefaultWhat it does
message“That name contains characters that could be confused with other letters.”the message for a refused name
additionalMappingsnonemore characters to refuse, as { character: "latin" }
rejectMixedScriptfalsealso refuse Latin letters mixed with letters of any other script
TypeScript
const lookalikes = createHomoglyphValidator({ rejectMixedScript: true });

await lookalikes("аdmin");  // → { available: false, message: "That name contains characters that could be confused with other letters." }
await lookalikes("tokyo東京");  // → { available: false } (Latin mixed with Han)
await lookalikes("sarah");  // → null

Lookalike characters covers which names it refuses, including ordinary words in Cyrillic and Greek.

#createInvisibleCharacterValidator()

createInvisibleCharacterValidator(options?): NamespaceValidator

A validator that refuses characters that change a name without showing: zero-width spaces and joiners, soft hyphens, variation selectors, and the controls that reverse the direction of text.

OptionDefaultWhat it does
message“That name contains invisible or direction-control characters.”the message for a refused name
rejectDefaultIgnorablestruerefuse Unicode’s default-ignorable characters, such as U+200B
rejectBidiControlstruerefuse direction marks, embeddings, overrides and isolates, such as U+202E
rejectCombiningMarksfalserefuse every combining mark, including the vowel signs of many scripts
TypeScript
const invisible = createInvisibleCharacterValidator();

await invisible("pay\u200Bpal");  // → { available: false, message: "That name contains invisible or direction-control characters." }
await invisible("\u202Egnp.exe");  // → { available: false }
await invisible("sarah");  // → null

See Invisible characters.

#createProfanityValidator()

createProfanityValidator(words, options?): NamespaceValidator

A validator that refuses a name containing a word from your list. By default it reads disguised letters as the letters they stand for, so 5h1t matches shit.

OptionDefaultWhat it does
message“That name is not allowed.”the message for a refused name
mode"evasion""evasion" reads lookalikes, digits and symbols as letters and drops separators; "basic" matches the name as it is
variantProfile"balanced"which digits and symbols count as letters; "aggressive" counts more
checkSubstringstruealso refuse names that contain a listed word
minSubstringLength4the fewest letters a listed word needs to be looked for inside a name; shorter words, and words common inside ordinary words, match only as words of their own (see Moderation)
maxFoldVariants64the most readings of one name that evasion mode compares
maplookalike letters from CONFUSABLE_MAP_FULLthe characters evasion mode reads as other letters
TypeScript
const moderation = createProfanityValidator(["badword"]);

await moderation("b4dword");  // → { available: false, message: "That name is not allowed." }
await moderation("my-badword-1");  // → { available: false }
await moderation("sarah");  // → null

Moderation covers the options and how to avoid refusing real names.

#createEnglishProfanityValidator()

createEnglishProfanityValidator(options?): NamespaceValidator, from namespace-guard/profanity-en

createProfanityValidator() with a curated list of 2,624 English words and phrases and PROFANITY_ALLOWLIST_EN, 2,278 names, places and common words that contain a listed word (scunthorpe, dickson). It takes the same options; an allowlist you pass is added to its own. The list is a separate import, so the core package doesn’t carry it.

In your app
import { createEnglishProfanityValidator } from "namespace-guard/profanity-en";

const moderation = createEnglishProfanityValidator({ checkSubstrings: false });

See The built-in English list.

#createPredicateValidator()

createPredicateValidator(predicate, options?): NamespaceValidator

Turns a yes-or-no test into a validator. The predicate receives the canonical form and returns true, or a promise of true, to refuse it.

OptionDefaultWhat it does
message“That name is not allowed.”the message for a refused name
transformnonechanges the name before the predicate sees it
TypeScript
const staffOnly = createPredicateValidator((name) => name.startsWith("staff-"), {
  message: "Names starting with staff- are for our team.",
});

await staffOnly("staff-sarah");  // → { available: false, message: "Names starting with staff- are for our team." }
await staffOnly("sarah");  // → null

Use it for a list kept elsewhere, or a moderation service. If the predicate throws, the guard refuses the name with the error’s message; see Use your own list or service.

#isLikelyUniqueViolationError()

isLikelyUniqueViolationError(error): boolean

Whether a database error means a duplicate key: PostgreSQL 23505, Prisma P2002, MySQL ER_DUP_ENTRY or errno 1062, SQLite SQLITE_CONSTRAINT, MongoDB 11000, or a message such as “duplicate key” or “unique constraint”. It also looks inside cause, parent, original and meta, where ORMs keep the driver’s error. claim() uses it unless you pass isUniqueViolation.

TypeScript
isLikelyUniqueViolationError({ code: "23505" });  // → true
isLikelyUniqueViolationError({ message: "Query failed", cause: { code: "P2002" } });  // → true
isLikelyUniqueViolationError(new Error("connection timed out"));  // → false

The test is broad: SQLite reports every constraint failure as “constraint failed”. See Recognise a unique violation.

#deriveNfkcTr39DivergenceVectors()

deriveNfkcTr39DivergenceVectors(map?): NfkcTr39DivergenceVector[]

The characters in a map that NFKC turns into a different ASCII letter or digit from the one the map gives, such as long ſ, which Unicode’s list reads as f and NFKC turns into s. Each is { char, codePoint, tr39, nfkc }. The map defaults to CONFUSABLE_MAP_FULL, and the result for it is exported as COMPOSABILITY_VECTORS.

TypeScript
deriveNfkcTr39DivergenceVectors()[0];  // → { char: "ſ", codePoint: "U+017F", tr39: "f", nfkc: "s" }
deriveNfkcTr39DivergenceVectors(CONFUSABLE_MAP).length;  // → 0

Use it to test a map of your own before relying on it after NFKC. See Composability vectors.

#Other exports

The rest of the package is data and entry points:

ExportImport fromSee
CONFUSABLE_MAP, CONFUSABLE_MAP_FULL, CONFUSABLE_MAP_CASED, CONFUSABLES_DATEnamespace-guardThe character maps
MEASURED_CONFUSABLESnamespace-guardMeasured lookalikes
LLM_CONFUSABLE_MAP and its countsnamespace-guardThe LLM map
COMPOSABILITY_VECTORS, COMPOSABILITY_VECTORS_COUNT, COMPOSABILITY_VECTOR_SUITE, NFKC_TR39_DIVERGENCE_VECTORSnamespace-guardComposability vectors
NAMESPACE_PROFILESnamespace-guardProfiles
DEFAULT_PROTECTED_TOKENSnamespace-guardenforceRisk()
CONFUSABLE_WEIGHTSnamespace-guard/confusable-weightsVisual weights
FONT_SPECIFIC_WEIGHTSnamespace-guard/font-specific-weightsWeights per font
PROFANITY_WORDS_EN and its detailsnamespace-guard/profanity-enThe profanity list
the adaptersnamespace-guard/adapters/*Adapters

The types are listed in TypeScript.