namespace-guarddocs

Reference

TypeScript

namespace-guard is written in TypeScript and ships its own type declarations, for ES modules and CommonJS alike, so there is nothing extra to install. Every type on this page is exported from namespace-guard. Import them with import type:

Importing types
import type { CheckResult, NamespaceAdapter, NamespaceConfig } from "namespace-guard";

The functions are typed too, so most of the time you won’t name a type at all: the result of guard.check() is already a CheckResult. Name one when you write something namespace-guard calls (an adapter, a validator) or pass a result on to code of your own.

#Configuration and adapters

TypeWhat it is
NamespaceConfigthe config for createNamespaceGuard() and createNamespaceGuardWithProfile(): sources, reserved, pattern, validators, suggest, cache, risk and the rest. See Configuration.
NamespaceSourceone table to check: { name, column, idColumn?, scopeKey? }
NamespaceAdapterwhat an adapter implements: { findOne(source, value, options?) }, resolving to a row or null. See Write your own adapter.
FindOneOptionsthe options the guard passes to findOne: { caseInsensitive?: boolean }
OwnershipScopethe scope you pass to check() and claim(), such as { id: user.id }: Record<string, string | null | undefined>
NamespaceProfileName"consumer-handle" | "org-slug" | "developer-id"
NamespaceProfilePresetwhat a profile sets: description, pattern, invalidMessage, normalizeUnicode, allowPurelyNumeric and risk. NAMESPACE_PROFILES holds the three.
SuggestStrategyNamea built-in suggestion strategy: "sequential", "random-digits", "suffix-words", "short-random", "scramble" or "similar"
NamespaceGuardthe guard itself, as createNamespaceGuard() returns it. Use it to type a guard you pass around.

#Results

CheckResult is a union. Test available, and TypeScript knows which fields exist:

Narrowing a CheckResult
import type { CheckResult } from "namespace-guard";

function describe(result: CheckResult): string {
  if (result.available) return "Free";
  switch (result.reason) {
    case "taken":
      return `${result.message} Try ${result.suggestions?.join(", ") ?? "another"}.`;
    case "reserved":
      return `${result.message} (${result.category})`;
    case "invalid":
      return result.message;
  }
}

The switch covers every reason, so TypeScript accepts the function without a final return. If a later version adds a reason, it tells you here.

TypeWhat it is
CheckResult{ available: true }, or { available: false, reason, message } with reason one of "invalid", "reserved" or "taken", and optionally source, category and suggestions
CheckManyOptionsoptions for checkMany(): { skipSuggestions?: boolean }, true by default
ClaimResult<T>what claim() returns: { claimed: true, normalized, value: T } with T the type your write returns, or { claimed: false, normalized, reason: "unavailable", message }
ClaimOptionsoptions for claim(): the risk options below, plus scope, isUniqueViolation and takenMessage
UniqueViolationDetector(error: unknown) => boolean, for ClaimOptions.isUniqueViolation. The default is isLikelyUniqueViolationError.
AssertClaimableOptionsoptions for assertClaimable(); the same as EnforceRiskOptions

#Validators

A validator receives the normalised name, and the name as typed in a second argument, and resolves to null to allow it or { available: false, message } to refuse it:

A validator of your own
import type { NamespaceValidator } from "namespace-guard";

const noDoubleHyphens: NamespaceValidator = async (value) =>
  value.includes("--") ? { available: false, message: "Use one hyphen at a time." } : null;
TypeWhat it is
NamespaceValidator(value: string, context?: { identifier: string }) => Promise<NamespaceValidatorResult>
NamespaceValidatorResult{ available: false; message: string } | null
PredicateValidatorOptionsoptions for createPredicateValidator(): message and transform
ProfanityValidatorOptionsoptions for createProfanityValidator() and createEnglishProfanityValidator()
ProfanityValidationMode"basic" | "evasion"
ProfanityVariantProfile"balanced" | "aggressive"
InvisibleCharacterValidatorOptionsoptions for createInvisibleCharacterValidator()

createHomoglyphValidator() takes { message?, additionalMappings?, rejectMixedScript? }, which has no exported name. If you need it, take it from the function:

An unnamed options type
type HomoglyphOptions = NonNullable<Parameters<typeof createHomoglyphValidator>[0]>;

#Risk

These describe how close a name comes to the names you protect. See Protect names.

TypeWhat it is
RiskCheckResultwhat checkRisk() returns: score (0 to 100), level, action, canBlock, reasons and matches
RiskMatchone protected name it came close to: target, score, distance, chainDepth, skeletonEqual, evidence and reasons
RiskMatchEvidencehow it matched: "exact", "lookalike", "lookalike-extension" or "typo"
RiskReasonone reason for the score: { code, message, weight }
RiskReasonCode"confusable-target", "skeleton-collision", "mixed-script", "invisible-character", "confusable-character", "divergent-mapping" or "deep-chain"
RiskLevel"low" | "medium" | "high"
RiskAction"allow" | "warn" | "block"
CheckRiskOptionsoptions for checkRisk(): protect, includeReserved, leetspeak, map, maxMatches, warnThreshold, blockThreshold
EnforceRiskOptionsCheckRiskOptions plus failOn ("block" or "warn") and messages
EnforceRiskResultwhat enforceRisk() returns: { allowed, action, message?, risk }

#Scanning text

For canonicalise(), scan() and isClean(). See LLM text.

TypeWhat it is
CanonicaliseOptionsthreshold, includeNovel, scripts, strategy ("mixed" or "all") and maxSizeRatio
ScanOptionsCanonicaliseOptions plus riskTerms
ScanResultwhat scan() returns: hasConfusables, count, findings and a summary with riskLevel ("none", "low", "medium" or "high")
ScanFindingone rewritten character: char, codepoint, script, latinEquivalent, visualScore, source ("tr39", "novel" or "fold"), index, word and mixedScript

#Comparing names and characters

For skeleton(), areConfusable(), confusableDistance(), detectCrossScriptRisk() and isDomainSpoof(). See Comparing names and Domains.

TypeWhat it is
SkeletonOptionsmap, preserveCase and ignoreDiacritics
AreConfusableOptionsSkeletonOptions plus weights and context ("identifier", "domain" or "all")
ConfusableWeightone measured pair: danger, stableDanger, cost, and flags for the source character (glyphReuse, xidContinue, idnaPvalid, tr39Allowed)
ConfusableWeightsRecord<string, Record<string, ConfusableWeight>>, keyed by source character, then target. CONFUSABLE_WEIGHTS has this type.
ConfusableDistanceOptionsmap, weights and context
ConfusableDistanceResultwhat confusableDistance() returns: distance, maxDistance, similarity (0 to 1), skeletonEqual, normalizedEqual, chainDepth, crossScriptCount, ignorableCount, divergenceCount and steps
ConfusableDistanceStepone step of the path: op, from, to, the indexes and cost, and for a step worth flagging, a reason such as "cross-script" or "visual-weight"
CrossScriptRiskResultwhat detectCrossScriptRisk() returns: scripts, crossScriptPairs and riskLevel
DomainSpoofOptionsmap, weights, minDanger and allowlist
DomainSpoofResultwhat isDomainSpoof() returns: spoof, and when a script matches, script, danger and substitutions
DomainSpoofSubstitutionone swapped character: index, from, to and similarity

#Data

TypeWhat it is
MeasuredConfusablean entry of MEASURED_CONFUSABLES: letter, target, tier and contexts
NfkcTr39DivergenceVectoran entry of NFKC_TR39_DIVERGENCE_VECTORS: { char, codePoint, tr39, nfkc }
ComposabilityVectorthe same type under the composability suite’s name; also exported from namespace-guard/composability-vectors

The entries of LLM_CONFUSABLE_MAP have no exported type name. Take it from the map:

The LLM map's entry type
import { LLM_CONFUSABLE_MAP } from "namespace-guard";

type LlmEntry = (typeof LLM_CONFUSABLE_MAP)[string][number];
// { latin, visualScore, source: "tr39" | "novel", script, codepoint, widthRatio?, heightRatio? }

Data and maps describes each export.

#A typed example

An adapter over a Map, a validator, a guard, and a claim() whose result carries the type the write returns:

Typed from end to end
import {
  createNamespaceGuardWithProfile,
  createHomoglyphValidator,
  type ClaimResult,
  type NamespaceAdapter,
  type NamespaceConfig,
  type NamespaceValidator,
} from "namespace-guard";

type User = { id: string; handle: string; handleCanonical: string };
const users = new Map<string, User>(); // keyed by canonical handle

const adapter: NamespaceAdapter = {
  async findOne(source, value) {
    const user = users.get(value);
    return user ? { id: user.id } : null;
  },
};

const noDoubleHyphens: NamespaceValidator = async (value) =>
  value.includes("--") ? { available: false, message: "Use one hyphen at a time." } : null;

const config: NamespaceConfig = {
  reserved: { system: ["admin", "api"], brand: ["yourapp"] },
  sources: [{ name: "user", column: "handleCanonical", scopeKey: "id" }],
  validators: [noDoubleHyphens, createHomoglyphValidator({ rejectMixedScript: true })],
  risk: { protect: ["yourapp"] },
};

const guard = createNamespaceGuardWithProfile("consumer-handle", config, adapter);

async function register(handle: string): Promise<ClaimResult<User>> {
  return guard.claim(handle, async (canonical) => {
    const user: User = { id: crypto.randomUUID(), handle, handleCanonical: canonical };
    users.set(canonical, user);
    return user;
  });
}

const result = await register("Sarah");
if (result.claimed) {
  console.log(result.value.handle);  // result.value is a User
} else {
  console.log(result.message);       // a message to show
}

This compiles under strict. The subpath exports have their own declarations: CONFUSABLE_WEIGHTS from namespace-guard/confusable-weights is ConfusableWeights, and FONT_SPECIFIC_WEIGHTS from namespace-guard/font-specific-weights is Record<string, ConfusableWeights>, one entry per font.