type LlmConfusableSource = "tr39" | "novel"; type LlmConfusableMapEntry = { latin: string; visualScore: number; source: LlmConfusableSource; script: string; codepoint: string; /** Width ratio between source and target at natural rendering size. Null if not measured. */ widthRatio?: number | null; /** Height ratio between source and target at natural rendering size. Null if not measured. */ heightRatio?: number | null; }; type LlmConfusableMap = Readonly>; declare const LLM_CONFUSABLE_MAP: LlmConfusableMap; declare const LLM_CONFUSABLE_MAP_PAIR_COUNT = 2218; declare const LLM_CONFUSABLE_MAP_CHAR_COUNT = 1947; declare const LLM_CONFUSABLE_MAP_SOURCE_COUNTS: Readonly<{ tr39: 1425; novel: 793; }>; /** A database table or model to check for slug/handle collisions. */ type NamespaceSource = { /** Table/model name (must match the adapter's lookup key) */ name: string; /** Column that holds the slug/handle */ column: string; /** Column name for the primary key (default: "id", or "_id" for Mongoose) */ idColumn?: string; /** Scope key for ownership checks - allows users to update their own slug without a false collision */ scopeKey?: string; }; /** Built-in suggestion strategy names. */ type SuggestStrategyName = "sequential" | "random-digits" | "suffix-words" | "short-random" | "scramble" | "similar"; /** Result type returned by async validators. */ type NamespaceValidatorResult = { available: false; message: string; } | null; /** Async validator hook type used by `NamespaceConfig.validators`. */ type NamespaceValidator = (value: string) => Promise; /** Configuration for a namespace guard instance. */ type NamespaceConfig = { /** Reserved names - flat list, Set, or categorized record */ reserved?: Set | string[] | Record; /** Data sources to check for collisions */ sources: NamespaceSource[]; /** Regex pattern for valid identifiers (default: lowercase alphanumeric + hyphens, 2-30 chars) */ pattern?: RegExp; /** Use case-insensitive matching in database queries (default: false) */ caseInsensitive?: boolean; /** Apply NFKC Unicode normalization during normalize() (default: true). * Collapses full-width characters, ligatures, and compatibility forms to their canonical equivalents. */ normalizeUnicode?: boolean; /** Allow purely numeric identifiers like "123" or "12-34" (default: true). * Set to false to reject them, matching Twitter/X handle rules. */ allowPurelyNumeric?: boolean; /** Custom error messages */ messages?: { invalid?: string; reserved?: string | Record; taken?: (sourceName: string) => string; /** Message shown when a purely numeric identifier is rejected (default: "Identifiers cannot be purely numeric.") */ purelyNumeric?: string; }; /** Async validation hooks - run after format/reserved checks, before DB */ validators?: NamespaceValidator[]; /** Enable conflict resolution suggestions when a slug is taken */ suggest?: { /** Named strategy, array of strategies to compose, or custom generator function (default: `["sequential", "random-digits"]`) */ strategy?: SuggestStrategyName | SuggestStrategyName[] | ((identifier: string) => string[]); /** Max suggestions to return (default: 3) */ max?: number; /** @deprecated Use `strategy` instead. Custom generator function. */ generate?: (identifier: string) => string[]; }; /** Enable in-memory caching of adapter lookups */ cache?: { /** Time-to-live in milliseconds (default: 5000) */ ttl?: number; /** Maximum number of cached entries before LRU eviction (default: 1000) */ maxSize?: number; }; /** Default risk policy for `checkRisk()` / `enforceRisk()` */ risk?: { /** Include reserved names as protected targets by default (default: true) */ includeReserved?: boolean; /** Default protected targets when none are passed to `checkRisk`/`enforceRisk` (default: none). */ protect?: string[]; /** Number of top matches returned by default (default: 3) */ maxMatches?: number; /** Default warn threshold for score/action mapping (default: 45) */ warnThreshold?: number; /** Default block threshold for score/action mapping (default: 70) */ blockThreshold?: number; }; }; /** Options passed to adapter `findOne` calls. */ type FindOneOptions = { /** Use case-insensitive matching */ caseInsensitive?: boolean; }; /** Database adapter interface - implement this for your ORM or query builder. */ type NamespaceAdapter = { findOne: (source: NamespaceSource, value: string, options?: FindOneOptions) => Promise | null>; }; /** Key-value pairs identifying the current user's records, used to skip self-collisions. */ type OwnershipScope = Record; /** * Result of a namespace availability check. * Either `{ available: true }` or an object with `reason`, `message`, and optional context. */ type CheckResult = { available: true; } | { available: false; reason: "invalid" | "reserved" | "taken"; message: string; source?: string; category?: string; suggestions?: string[]; }; /** Options for `checkMany()`. */ type CheckManyOptions = { /** Skip suggestion generation for taken identifiers (default: `true`). */ skipSuggestions?: boolean; }; /** Evasion handling mode for `createProfanityValidator()`. */ type ProfanityValidationMode = "basic" | "evasion"; /** Substitute-folding strictness for `createProfanityValidator()`. */ type ProfanityVariantProfile = "balanced" | "aggressive"; /** Options for `createProfanityValidator()`. */ type ProfanityValidatorOptions = { /** Custom rejection message (default: "That name is not allowed."). */ message?: string; /** Check if identifier contains a blocked word as a substring (default: `true`). */ checkSubstrings?: boolean; /** `basic`: lowercase matching only. `evasion`: Unicode+substitute folding (default: `evasion`). */ mode?: ProfanityValidationMode; /** Variant strictness for evasion mode (default: `balanced`). */ variantProfile?: ProfanityVariantProfile; /** Minimum blocked-word length for substring checks (default: `4`). Exact matches are always checked. */ minSubstringLength?: number; /** Confusable map used for evasion folding (default: `CONFUSABLE_MAP_FULL`). */ map?: Record; /** Max folded candidates generated per value in evasion mode (default: `64`). */ maxFoldVariants?: number; }; /** Options for `createInvisibleCharacterValidator()`. */ type InvisibleCharacterValidatorOptions = { /** Custom rejection message. */ message?: string; /** Reject Unicode Default_Ignorable_Code_Point characters (default: `true`). */ rejectDefaultIgnorables?: boolean; /** Reject bidi direction/control characters (default: `true`). */ rejectBidiControls?: boolean; /** Reject combining marks (Unicode category `M*`) often used for visual obfuscation (default: `false`). */ rejectCombiningMarks?: boolean; }; /** Options for `canonicalise()` LLM preprocessing. */ type CanonicaliseOptions = { /** Minimum visual score required for replacement (default: `0.7`). */ threshold?: number; /** Include confusable-vision novel discoveries in addition to TR39 mappings (default: `true`). */ includeNovel?: boolean; /** Restrict replacement to specific source scripts (case-insensitive, e.g. `["Cyrillic", "Greek"]`). */ scripts?: string[]; /** * Canonicalisation strategy (default: `"mixed"`). * * - `"mixed"` -- only replace confusable characters inside tokens that already * contain Latin letters. Standalone non-Latin words (e.g. "Москва") are * preserved. Safe for multilingual text. * * - `"all"` -- replace every confusable character regardless of surrounding * context. Use this when the document is known to be Latin-script (e.g. * an English contract) and you want to catch attackers who substitute * every character in a word. */ strategy?: "mixed" | "all"; /** * Maximum allowed width or height ratio between source and target at natural * rendering size (default: `3.0`). Pairs where the source character is more * than this many times wider or taller than the Latin target are skipped, * because the size difference would be visible in running text even if the * shapes match after normalisation. * * Set to `Infinity` to disable size-ratio filtering. Set to `2.0` for * stricter filtering. Only applies to novel (non-TR39) pairs that have * measured size ratios. */ maxSizeRatio?: number; }; /** Options for `scan()` and `isClean()`. */ type ScanOptions = CanonicaliseOptions & { /** Optional list of high-value terms used to raise `riskLevel` when targeted (default: built-in legal/financial list). */ riskTerms?: string[]; }; /** Single confusable finding returned by `scan()`. */ type ScanFinding = { /** The confusable character found in the input. */ char: string; /** Codepoint label in `U+XXXX` format. */ codepoint: string; /** Script name of the source character. */ script: string; /** Canonical Latin equivalent selected by the lookup table. */ latinEquivalent: string; /** Visual similarity score for this mapping (0–1, from RaySpace measurement). */ visualScore: number; /** Mapping source (`tr39` baseline or `novel` discovery). */ source: "tr39" | "novel"; /** UTF-16 code-unit offset in the input string. */ index: number; /** Token/word containing this character. */ word: string; /** Whether the token mixes Latin and non-Latin letters. */ mixedScript: boolean; }; /** Structured confusable scan result for LLM preprocessing pipelines. */ type ScanResult = { /** Whether any confusable mapping candidates were detected. */ hasConfusables: boolean; /** Number of findings in `findings`. */ count: number; /** Detailed findings with script/source/position metadata. */ findings: ScanFinding[]; /** Aggregate scan summary for policy and logging. */ summary: { /** Number of distinct confusable characters found. */ distinctChars: number; /** Number of distinct words/tokens affected. */ wordsAffected: number; /** Distinct scripts detected among findings. */ scriptsDetected: string[]; /** Heuristic risk level from confusable density + targeting. */ riskLevel: "none" | "low" | "medium" | "high"; }; }; /** Options for the `skeleton()` and `areConfusable()` functions. */ type SkeletonOptions = { /** Confusable character map to use. * Default: `CONFUSABLE_MAP_FULL` (complete TR39 map, no NFKC filtering). * Pass `CONFUSABLE_MAP` if your pipeline runs NFKC before calling skeleton(). */ map?: Record; }; /** Options for `areConfusable()` with optional weight-based matching. */ type AreConfusableOptions = SkeletonOptions & { /** Optional measured visual weights for weight-based confusable matching. * When provided, character pairs in the weight table are also considered confusable. */ weights?: ConfusableWeights; /** Filter weights by deployment context. * - `'identifier'`: only apply weights for XID_Continue sources * - `'domain'`: only apply weights for IDNA PVALID sources * - `'all'` (default): apply all weights regardless of properties */ context?: "identifier" | "domain" | "all"; }; /** Measured visual weight for a single confusable pair. */ type ConfusableWeight = { /** Maximum visual similarity across all font comparisons (attacker perspective). */ danger: number; /** 95th percentile visual similarity across all font comparisons (defender perspective). */ stableDanger: number; /** 1 - stableDanger, clamped [0, 1]. Lower cost = more dangerous. */ cost: number; /** True if font cmap reveals intentional glyph reuse. */ glyphReuse?: boolean; /** Source char is valid in UAX #31 identifiers (XID_Continue). */ xidContinue?: boolean; /** Source char is PVALID in IDNA 2008 (relevant for domain spoofing). */ idnaPvalid?: boolean; /** Source char is TR39 Identifier_Status=Allowed. */ tr39Allowed?: boolean; }; /** Lookup table of measured visual weights, keyed by source char then target char. */ type ConfusableWeights = Record>; /** Options for `confusableDistance()`. */ type ConfusableDistanceOptions = { /** Confusable character map to use (default: `CONFUSABLE_MAP_FULL`). */ map?: Record; /** Optional measured visual weights from confusable-vision scoring. * When provided, TR39 pairs use measured cost instead of hardcoded 0.35, * and novel pairs (not in TR39 map) use their visual-weight cost. */ weights?: ConfusableWeights; /** Filter weights by deployment context. * - `'identifier'`: only apply weights for XID_Continue sources * - `'domain'`: only apply weights for IDNA PVALID sources * - `'all'` (default): apply all weights regardless of properties */ context?: "identifier" | "domain" | "all"; }; /** Step-by-step edit operation in a confusable distance path. */ type ConfusableDistanceStep = { /** Operation type for this path step. */ op: "match" | "substitution" | "confusable-substitution" | "insertion" | "deletion"; /** Source character (for substitution/deletion). */ from?: string; /** Target character (for substitution/insertion). */ to?: string; /** Zero-based index in the source string for this operation. */ fromIndex: number; /** Zero-based index in the target string for this operation. */ toIndex: number; /** Weighted operation cost. */ cost: number; /** Shared prototype when op is `confusable-substitution`. */ prototype?: string; /** True when substitution crosses Unicode scripts (e.g. Latin to Cyrillic). */ crossScript?: boolean; /** True when substitution uses a known NFKC/TR39 divergent mapping. */ divergence?: boolean; /** Human-readable signal for high-risk operations. */ reason?: "default-ignorable" | "cross-script" | "nfkc-divergence" | "nfkc-equivalent" | "visual-weight"; }; /** Result of weighted confusable distance analysis between two strings. */ type ConfusableDistanceResult = { /** Weighted edit distance (lower means more confusable). */ distance: number; /** Maximum baseline distance used for similarity scaling. */ maxDistance: number; /** Similarity score in [0, 1], where 1 is most similar. */ similarity: number; /** Whether TR39 skeletons are equal. */ skeletonEqual: boolean; /** Whether NFKC + lowercase forms are equal. */ normalizedEqual: boolean; /** Number of non-trivial path operations (attack chain depth proxy). */ chainDepth: number; /** Number of cross-script confusable substitutions in the path. */ crossScriptCount: number; /** Number of default-ignorable insertions/deletions in the path. */ ignorableCount: number; /** Number of substitutions involving NFKC/TR39 divergent mappings. */ divergenceCount: number; /** Weighted shortest edit path used to compute the distance. */ steps: ConfusableDistanceStep[]; }; /** A character-level mapping where TR39 and NFKC disagree on ASCII prototype. */ type NfkcTr39DivergenceVector = { /** Source character from confusables data. */ char: string; /** Unicode scalar value formatted as `U+XXXX`. */ codePoint: string; /** TR39 confusable target from the selected map. */ tr39: string; /** NFKC lowercase result for the source character. */ nfkc: string; }; /** Canonical composability regression vector (alias of `NfkcTr39DivergenceVector`). */ type ComposabilityVector = NfkcTr39DivergenceVector; /** Risk reason code returned by `checkRisk()`. */ type RiskReasonCode = "confusable-target" | "skeleton-collision" | "mixed-script" | "invisible-character" | "confusable-character" | "divergent-mapping" | "deep-chain"; /** Structured reason contributing to a risk score. */ type RiskReason = { code: RiskReasonCode; message: string; weight: number; }; /** Risk levels returned by `checkRisk()`. */ type RiskLevel = "low" | "medium" | "high"; /** Policy action derived from the configured thresholds. */ type RiskAction = "allow" | "warn" | "block"; /** A nearest protected target returned by risk scoring. */ type RiskMatch = { target: string; score: number; distance: number; chainDepth: number; skeletonEqual: boolean; reasons: string[]; }; /** Options for `guard.checkRisk()`. */ type CheckRiskOptions = { /** Additional high-value identifiers to protect against confusable variants. */ protect?: string[]; /** Include configured reserved names in the protected target set (default: true). */ includeReserved?: boolean; /** Confusable map used for skeletoning and distance scoring (default: `CONFUSABLE_MAP_FULL`). */ map?: Record; /** Number of highest-risk matches to return (default: 3). */ maxMatches?: number; /** Score threshold where action transitions from `allow` to `warn` (default: 45). */ warnThreshold?: number; /** Score threshold where action transitions from `warn` to `block` (default: 70). */ blockThreshold?: number; }; /** Output of `guard.checkRisk()`. */ type RiskCheckResult = { identifier: string; normalized: string; score: number; level: RiskLevel; action: RiskAction; reasons: RiskReason[]; matches: RiskMatch[]; }; /** Options for `guard.enforceRisk()`. */ type EnforceRiskOptions = CheckRiskOptions & { /** Deny mode. "block" denies only block-level risk; "warn" denies warn+block. */ failOn?: "block" | "warn"; /** Custom messages for denied outcomes. */ messages?: { warn?: string; block?: string; }; }; /** Options for `guard.assertClaimable()`. */ type AssertClaimableOptions = EnforceRiskOptions; /** Result of `guard.enforceRisk()`. */ type EnforceRiskResult = { allowed: boolean; action: RiskAction; message?: string; risk: RiskCheckResult; }; /** Predicate for detecting duplicate-key / unique-constraint errors from write operations. */ type UniqueViolationDetector = (error: unknown) => boolean; /** Options for `guard.claim()`. */ type ClaimOptions = AssertClaimableOptions & { /** Ownership scope passed to availability checks. */ scope?: OwnershipScope; /** Optional custom detector for duplicate-key/unique-constraint write errors. */ isUniqueViolation?: UniqueViolationDetector; /** Message used when write fails due to a unique violation (default: "That name is already in use."). */ takenMessage?: string; }; /** Result of `guard.claim()`. */ type ClaimResult = { claimed: true; normalized: string; value: T; } | { claimed: false; normalized: string; reason: "unavailable"; message: string; }; /** * Best-effort detection of duplicate-key / unique-constraint errors across * common data layers (Postgres, MySQL, SQLite, Prisma, MongoDB). */ declare function isLikelyUniqueViolationError(error: unknown): boolean; /** Built-in profile names for practical defaults. */ type NamespaceProfileName = "consumer-handle" | "org-slug" | "developer-id"; /** Profile preset definition. */ type NamespaceProfilePreset = { description: string; pattern: RegExp; normalizeUnicode: boolean; allowPurelyNumeric: boolean; risk: Required>; }; /** Default high-value targets used when enforcing risk without explicit protect targets. */ declare const DEFAULT_PROTECTED_TOKENS: string[]; /** Practical preset profiles for common namespace types. */ declare const NAMESPACE_PROFILES: Record; /** * Normalize a raw identifier: trims whitespace, applies NFKC Unicode normalization, * lowercases, and strips leading `@` symbols. * * NFKC normalization collapses full-width characters, ligatures, superscripts, * and other compatibility forms to their canonical equivalents. This is a no-op * for ASCII-only input. * * @param raw - The raw user input * @param options - Optional settings * @param options.unicode - Apply NFKC Unicode normalization (default: true) * @returns The normalized identifier * * @example * ```ts * normalize(" @Sarah "); // "sarah" * normalize("ACME-Corp"); // "acme-corp" * normalize("\uff48\uff45\uff4c\uff4c\uff4f"); // "hello" (full-width → ASCII) * ``` */ declare function normalize(raw: string, options?: { unicode?: boolean; }): string; /** Options for `createPredicateValidator()`. */ type PredicateValidatorOptions = { /** Custom rejection message (default: "That name is not allowed."). */ message?: string; /** Optional transform applied before passing input to the predicate. */ transform?: (value: string) => string; }; /** * Wrap a sync/async boolean predicate as a namespace validator. * * Useful for integrating third-party moderation/profanity libraries without * adding dependencies to namespace-guard itself. * * @param predicate - Returns `true` when the value should be blocked * @param options - Optional rejection message and value transform * @returns A validator compatible with `config.validators` */ declare function createPredicateValidator(predicate: (value: string) => boolean | Promise, options?: PredicateValidatorOptions): NamespaceValidator; /** * Create a validator that rejects identifiers containing profanity or offensive words. * * Supply your own word list - no words are bundled with the library. * The returned function is compatible with `config.validators`. * * @param words - Array of words to block * @param options - Optional settings * @param options.message - Custom rejection message (default: "That name is not allowed.") * @param options.checkSubstrings - Check if identifier contains a blocked word as a substring (default: true) * @param options.mode - `basic` (lowercase only) or `evasion` (Unicode/substitute folding) (default: `evasion`) * @param options.variantProfile - `balanced` (precision-first) or `aggressive` (broader substitutes) (default: `balanced`) * @param options.minSubstringLength - Minimum blocked-word length used in substring checks (default: `4`) * @param options.map - Confusable map used by `mode: "evasion"` (default: `CONFUSABLE_MAP_FULL`) * @param options.maxFoldVariants - Max folded candidates considered in evasion mode (default: 64) * @returns An async validator function for use in `config.validators` * * @example * ```ts * const guard = createNamespaceGuard({ * reserved: ["admin"], * sources: [{ name: "user", column: "handle" }], * validators: [ * createProfanityValidator(["badword", "offensive"], { * message: "Please choose an appropriate name.", * }), * ], * }, adapter); * ``` */ declare function createProfanityValidator(words: readonly string[], options?: ProfanityValidatorOptions): NamespaceValidator; /** * Mapping of visually confusable Unicode characters to their Latin/digit equivalents. * Generated from Unicode TR39 confusables.txt + supplemental Latin small capitals. * Covers every single-character mapping to a lowercase Latin letter or digit, * excluding characters already handled by NFKC normalization (either collapsed * to the same target, or mapped to a different valid Latin char/digit). * Regenerate: `npx tsx scripts/generate-confusables.ts` * * Data source: Unicode confusables.txt (https://unicode.org/Public/security/latest/confusables.txt) * Copyright 1991-Present Unicode, Inc. Licensed under the Unicode License v3. * See https://www.unicode.org/terms_of_use.html */ declare const CONFUSABLE_MAP: Record; /** * Complete TR39 confusable mapping: every single-character mapping to a * lowercase Latin letter or digit from confusables.txt, with no NFKC filtering. * * Use this when your pipeline does NOT run NFKC normalization before confusable * detection (which is most real-world systems: TR39 skeleton uses NFD, Chromium * uses NFD, Rust uses NFC, django-registration uses no normalization at all). * * Includes ~1,400 entries vs CONFUSABLE_MAP's ~613 NFKC-deduped entries. * The additional entries cover characters that NFKC normalization would handle * (mathematical alphanumerics, fullwidth forms, etc.) plus the 31 entries where * TR39 and NFKC disagree on the target letter. * * Regenerate: `npx tsx scripts/generate-confusables.ts` * * Data source: Unicode confusables.txt (https://unicode.org/Public/security/latest/confusables.txt) * Copyright 1991-Present Unicode, Inc. Licensed under the Unicode License v3. * See https://www.unicode.org/terms_of_use.html */ declare const CONFUSABLE_MAP_FULL: Record; /** * Derive the set of characters where TR39 prototype mapping and NFKC lowercase * mapping disagree on single ASCII letter/digit outcomes. */ declare function deriveNfkcTr39DivergenceVectors(map?: Record): NfkcTr39DivergenceVector[]; /** * Built-in composability regression corpus: * characters where TR39 confusables and NFKC disagree on ASCII targets. */ declare const NFKC_TR39_DIVERGENCE_VECTORS: NfkcTr39DivergenceVector[]; /** Named composability regression suite (TR39-full vs NFKC lowercase). */ declare const COMPOSABILITY_VECTOR_SUITE = "nfkc-tr39-divergence-v1"; /** Named alias for `NFKC_TR39_DIVERGENCE_VECTORS` for cross-library regression tests. */ declare const COMPOSABILITY_VECTORS: readonly ComposabilityVector[]; /** Number of vectors in the composability regression suite. */ declare const COMPOSABILITY_VECTORS_COUNT: number; /** * Create a validator that rejects identifiers containing homoglyph/confusable characters. * * Catches spoofing attacks where characters from other scripts are substituted for * visually identical Latin characters (e.g., Cyrillic "а" for Latin "a" in "admin"). * Uses a comprehensive mapping of 613 character pairs generated from Unicode TR39 * confusables.txt, covering Cyrillic, Greek, Armenian, Cherokee, IPA, Latin small * capitals, Canadian Syllabics, Georgian, Lisu, Coptic, and many other scripts. * * @param options - Optional settings * @param options.message - Custom rejection message (default: "That name contains characters that could be confused with other letters.") * @param options.additionalMappings - Extra confusable pairs to merge with the built-in map * @param options.rejectMixedScript - Also reject identifiers that mix Latin with non-Latin characters from any covered script (Cyrillic, Greek, Armenian, Hebrew, Arabic, Georgian, Cherokee, Canadian Syllabics, Ethiopic, Coptic, Lisu, and more) (default: false) * @returns An async validator function for use in `config.validators` * * @example * ```ts * const guard = createNamespaceGuard({ * sources: [{ name: "user", column: "handle" }], * validators: [ * createHomoglyphValidator(), * ], * }, adapter); * ``` */ declare function createHomoglyphValidator(options?: { message?: string; additionalMappings?: Record; rejectMixedScript?: boolean; }): NamespaceValidator; /** * Create a validator that rejects invisible/control Unicode characters often used * for evasion and text-direction spoofing (Trojan Source style). * * @param options - Optional settings * @param options.message - Custom rejection message * @param options.rejectDefaultIgnorables - Reject Unicode Default_Ignorable_Code_Point characters (default: true) * @param options.rejectBidiControls - Reject bidi direction/control characters (default: true) * @param options.rejectCombiningMarks - Reject combining marks (default: false) * @returns An async validator function for use in `config.validators` */ declare function createInvisibleCharacterValidator(options?: InvisibleCharacterValidatorOptions): NamespaceValidator; /** * Canonicalise confusable characters in text for LLM preprocessing. * * With the default `strategy: "mixed"`, only rewrites characters inside tokens * that already contain Latin letters. Standalone non-Latin words are preserved * to reduce false positives in multilingual text. * * With `strategy: "all"`, rewrites every confusable character regardless of * context. Use this when the document is known to be Latin-script. */ declare function canonicalise(text: string, options?: CanonicaliseOptions): string; /** * Scan text for confusable characters and return structured findings + risk summary. */ declare function scan(text: string, options?: ScanOptions): ScanResult; /** * Fast gate for LLM pipelines. * * With the default `strategy: "mixed"`, returns `false` as soon as a * mixed-script confusable substitution is found. Standalone non-Latin * words do not fail this gate. * * With `strategy: "all"`, returns `false` if any confusable character is * found, regardless of surrounding context. */ declare function isClean(text: string, options?: ScanOptions): boolean; /** * Compute the TR39 Section 4 skeleton of a string for confusable comparison. * * Implements `internalSkeleton`: * 1. NFD normalize * 2. Remove Default_Ignorable_Code_Point characters * 3. Replace each character via the confusable map * 4. Reapply NFD * 5. Lowercase * * The default map is `CONFUSABLE_MAP_FULL` (the complete TR39 mapping without * NFKC filtering), which matches the NFD-based pipeline used by ICU, Chromium, * and the TR39 spec itself. Pass `{ map: CONFUSABLE_MAP }` if your pipeline * runs NFKC normalization before calling skeleton(). * * @param input - The string to skeletonize * @param options - Optional settings (custom confusable map) * @returns The skeleton string for comparison * * @example * ```ts * skeleton("paypal") === skeleton("\u0440\u0430ypal") // true (Cyrillic р/а) * skeleton("pay\u200Bpal") === skeleton("paypal") // true (zero-width stripped) * ``` */ declare function skeleton(input: string, options?: SkeletonOptions): string; /** * Compute a weighted confusable distance between two strings. * * Uses a shortest-path edit model where substitutions between characters that share * a TR39 prototype are low cost, default-ignorable insertions/deletions are very low * cost, and cross-script confusable substitutions increase risk and chain depth. * * This keeps TR39 skeleton equality as the baseline while exposing a graded score. */ declare function confusableDistance(a: string, b: string, options?: ConfusableDistanceOptions): ConfusableDistanceResult; /** * Check whether two strings are visually confusable. * * Without `weights`, compares TR39 skeletons only (backward compatible). * With `weights`, also checks whether any character pair in `a` x `b` * has a measured visual weight, enabling cross-script confusable detection. * * @param a - First string * @param b - Second string * @param options - Optional settings (custom confusable map, weights, context) * @returns `true` if the strings are considered confusable * * @example * ```ts * areConfusable("paypal", "\u0440\u0430ypal") // true (skeleton match) * areConfusable("hello", "world") // false * areConfusable("\u1175", "\u4E28", { weights }) // true (cross-script weight) * areConfusable("\u1175", "\u4E28") // false (no weights = skeleton only) * ``` */ declare function areConfusable(a: string, b: string, options?: AreConfusableOptions): boolean; /** Result of cross-script risk analysis on an identifier. */ type CrossScriptRiskResult = { /** Distinct Unicode scripts detected in the identifier. */ scripts: string[]; /** Character pairs from different scripts that are visually confusable. */ crossScriptPairs: Array<{ a: { char: string; script: string; }; b: { char: string; script: string; }; visualScore: number; }>; /** Overall risk level: "none" (single script), "low" (matches below 0.8), "high" (visualScore >= 0.8 or 3+ matches). */ riskLevel: "none" | "low" | "high"; }; /** * Detect cross-script confusable risk in an identifier. * * Analyzes an identifier for characters from multiple Unicode scripts and * checks whether any cross-script character pairs are visually confusable * according to measured weights. * * @param identifier - The identifier to analyze * @param options - Optional settings (weights for cross-script lookup) * @returns Cross-script risk analysis result * * @example * ```ts * detectCrossScriptRisk("hello") // riskLevel: "none" * detectCrossScriptRisk("\u1175\u4E28", { weights }) // riskLevel: "high" * ``` */ declare function detectCrossScriptRisk(identifier: string, options?: { weights?: ConfusableWeights; }): CrossScriptRiskResult; /** A single character-level substitution in a domain spoof. */ type DomainSpoofSubstitution = { /** Position in the label. */ index: number; /** The target's character at this position. */ from: string; /** The label's character (the confusable replacement). */ to: string; /** Visual similarity between the two characters (0–1). */ similarity: number; }; /** Result of {@link isDomainSpoof}. */ type DomainSpoofResult = { /** Opinionated verdict: `true` when `danger >= minDanger` and the label is * a single-script confusable of the target. */ spoof: boolean; /** Script of the spoofing label (set whenever a full-script match is found, * even if `danger` is below the threshold). */ script?: string; /** Average visual similarity across substitutions (0–1). * Always set when a script match is found, regardless of `spoof`. * Callers who want finer control can ignore `spoof` and threshold on * `danger` directly. */ danger?: number; /** Per-character substitution details. */ substitutions?: DomainSpoofSubstitution[]; }; /** Options for {@link isDomainSpoof}. */ type DomainSpoofOptions = { /** Confusable character map (default: `CONFUSABLE_MAP_FULL`). */ map?: Record; /** Measured visual weights for similarity scoring. */ weights?: ConfusableWeights; /** Minimum average danger for `spoof` to be `true` (default `0.5`). * Set higher (e.g. `0.7`) for fewer false positives. * The `danger` score is always returned regardless — callers can apply * their own threshold. */ minDanger?: number; /** Known-legitimate non-Latin labels to skip (e.g. `["банк", "москва"]`). * Checked after NFKC + lowercase normalisation. */ allowlist?: string[]; }; /** * Check whether a domain label is a realistic spoof of a target label. * * ICANN registrars enforce single-script labels under IDN rules, so a * mixed-script domain like `pаypal.com` (Cyrillic а + Latin) cannot actually * be registered. This function only flags threats that could produce * registrable domain names: * * - The label must be **single-script** (or single-script + Common characters * like digits and hyphens). * - Every non-Common character must be a **confusable** of the corresponding * character in the target, verified against the confusable map and optional * measured weights. * * Mixed-script substitutions are deliberately excluded because registrars * reject them. * * @param label - The suspicious domain label to check (e.g. Cyrillic "раураl") * @param target - The legitimate domain label (e.g. "paypal") * @param options - Map, weights, threshold, and allowlist settings * @returns Spoof analysis with opinionated verdict and detailed scores * * @example * ```ts * import { isDomainSpoof } from "namespace-guard"; * import { CONFUSABLE_WEIGHTS } from "namespace-guard/confusable-weights"; * * // Full-Cyrillic lookalike — realistic, registrable spoof * isDomainSpoof("\u0440\u0430\u0443\u0440\u0430\u04cf", "paypal", * { weights: CONFUSABLE_WEIGHTS }); * // { spoof: true, script: "cyrillic", danger: 0.91, substitutions: [...] } * * // Mixed-script — cannot be registered, not a spoof * isDomainSpoof("\u0440aypal", "paypal", * { weights: CONFUSABLE_WEIGHTS }); * // { spoof: false } * ``` */ declare function isDomainSpoof(label: string, target: string, options?: DomainSpoofOptions): DomainSpoofResult; /** * Create a guard with a built-in profile preset for practical defaults. * * Profile values apply first; explicit `config` values override the preset. */ declare function createNamespaceGuardWithProfile(profileName: NamespaceProfileName, config: NamespaceConfig, adapter: NamespaceAdapter): { normalize: typeof normalize; validateFormat: (identifier: string) => string | null; validateFormatOnly: (identifier: string) => string | null; check: (identifier: string, scope?: OwnershipScope, options?: { skipSuggestions?: boolean; }) => Promise; assertAvailable: (identifier: string, scope?: OwnershipScope) => Promise; assertClaimable: (identifier: string, scope?: OwnershipScope, options?: AssertClaimableOptions) => Promise; claim: (identifier: string, write: (normalized: string) => Promise, options?: ClaimOptions) => Promise>; checkMany: (identifiers: string[], scope?: OwnershipScope, options?: CheckManyOptions) => Promise>; checkRisk: (identifier: string, options?: CheckRiskOptions) => RiskCheckResult; enforceRisk: (identifier: string, options?: EnforceRiskOptions) => EnforceRiskResult; clearCache: () => void; cacheStats: () => { size: number; hits: number; misses: number; }; }; /** * Create a namespace guard instance for checking slug/handle uniqueness * across multiple database tables with reserved name protection. * * @param config - Reserved names, data sources, validation pattern, and optional features * @param adapter - Database adapter implementing the `findOne` lookup (use a built-in adapter or write your own) * @returns A guard with `check`, `checkMany`, `checkRisk`, `enforceRisk`, `assertAvailable`, `assertClaimable`, `claim`, `validateFormat`, `validateFormatOnly`, `clearCache`, and `cacheStats` methods * * @example * ```ts * import { createNamespaceGuard } from "namespace-guard"; * import { createPrismaAdapter } from "namespace-guard/adapters/prisma"; * * const guard = createNamespaceGuard( * { * reserved: ["admin", "api", "settings"], * sources: [ * { name: "user", column: "handle", scopeKey: "id" }, * { name: "organization", column: "slug", scopeKey: "id" }, * ], * }, * createPrismaAdapter(prisma) * ); * * const result = await guard.check("my-slug"); * if (result.available) { * // safe to use * } * ``` */ declare function createNamespaceGuard(config: NamespaceConfig, adapter: NamespaceAdapter): { normalize: typeof normalize; validateFormat: (identifier: string) => string | null; validateFormatOnly: (identifier: string) => string | null; check: (identifier: string, scope?: OwnershipScope, options?: { skipSuggestions?: boolean; }) => Promise; assertAvailable: (identifier: string, scope?: OwnershipScope) => Promise; assertClaimable: (identifier: string, scope?: OwnershipScope, options?: AssertClaimableOptions) => Promise; claim: (identifier: string, write: (normalized: string) => Promise, options?: ClaimOptions) => Promise>; checkMany: (identifiers: string[], scope?: OwnershipScope, options?: CheckManyOptions) => Promise>; checkRisk: (identifier: string, options?: CheckRiskOptions) => RiskCheckResult; enforceRisk: (identifier: string, options?: EnforceRiskOptions) => EnforceRiskResult; clearCache: () => void; cacheStats: () => { size: number; hits: number; misses: number; }; }; /** The guard instance returned by `createNamespaceGuard`. */ type NamespaceGuard = ReturnType; export { type AreConfusableOptions, type AssertClaimableOptions, COMPOSABILITY_VECTORS, COMPOSABILITY_VECTORS_COUNT, COMPOSABILITY_VECTOR_SUITE, CONFUSABLE_MAP, CONFUSABLE_MAP_FULL, type CanonicaliseOptions, type CheckManyOptions, type CheckResult, type CheckRiskOptions, type ClaimOptions, type ClaimResult, type ComposabilityVector, type ConfusableDistanceOptions, type ConfusableDistanceResult, type ConfusableDistanceStep, type ConfusableWeight, type ConfusableWeights, type CrossScriptRiskResult, DEFAULT_PROTECTED_TOKENS, type DomainSpoofOptions, type DomainSpoofResult, type DomainSpoofSubstitution, type EnforceRiskOptions, type EnforceRiskResult, type FindOneOptions, type InvisibleCharacterValidatorOptions, LLM_CONFUSABLE_MAP, LLM_CONFUSABLE_MAP_CHAR_COUNT, LLM_CONFUSABLE_MAP_PAIR_COUNT, LLM_CONFUSABLE_MAP_SOURCE_COUNTS, NAMESPACE_PROFILES, NFKC_TR39_DIVERGENCE_VECTORS, type NamespaceAdapter, type NamespaceConfig, type NamespaceGuard, type NamespaceProfileName, type NamespaceProfilePreset, type NamespaceSource, type NamespaceValidator, type NamespaceValidatorResult, type NfkcTr39DivergenceVector, type OwnershipScope, type PredicateValidatorOptions, type ProfanityValidationMode, type ProfanityValidatorOptions, type ProfanityVariantProfile, type RiskAction, type RiskCheckResult, type RiskLevel, type RiskMatch, type RiskReason, type RiskReasonCode, type ScanFinding, type ScanOptions, type ScanResult, type SkeletonOptions, type SuggestStrategyName, type UniqueViolationDetector, areConfusable, canonicalise, confusableDistance, createHomoglyphValidator, createInvisibleCharacterValidator, createNamespaceGuard, createNamespaceGuardWithProfile, createPredicateValidator, createProfanityValidator, deriveNfkcTr39DivergenceVectors, detectCrossScriptRisk, isClean, isDomainSpoof, isLikelyUniqueViolationError, normalize, scan, skeleton };