{"id":48764656,"url":"https://github.com/paultendo/namespace-guard","last_synced_at":"2026-04-13T07:47:38.840Z","repository":{"id":339481943,"uuid":"1162163719","full_name":"paultendo/namespace-guard","owner":"paultendo","description":"Check slug/handle uniqueness across multiple database tables with reserved name protection.","archived":false,"fork":false,"pushed_at":"2026-02-26T23:12:59.000Z","size":953,"stargazers_count":5,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-27T04:46:39.632Z","etag":null,"topics":["anti-spoofing","confusable","drizzle","homoglyph","knex","kysely","multi-tenant","namespace","nodejs","prisma","profanity-filter","security","sequelize","slug","tr39","typeorm","typescript","unicode","username","validation"],"latest_commit_sha":null,"homepage":"https://paultendo.github.io/namespace-guard/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/paultendo.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"paultendo","buy_me_a_coffee":"paultendo"}},"created_at":"2026-02-19T23:55:32.000Z","updated_at":"2026-02-26T23:13:03.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/paultendo/namespace-guard","commit_stats":null,"previous_names":["paultendo/namespace-guard"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/paultendo/namespace-guard","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paultendo%2Fnamespace-guard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paultendo%2Fnamespace-guard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paultendo%2Fnamespace-guard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paultendo%2Fnamespace-guard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/paultendo","download_url":"https://codeload.github.com/paultendo/namespace-guard/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paultendo%2Fnamespace-guard/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31744404,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-13T06:26:45.479Z","status":"ssl_error","status_checked_at":"2026-04-13T06:26:44.645Z","response_time":93,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["anti-spoofing","confusable","drizzle","homoglyph","knex","kysely","multi-tenant","namespace","nodejs","prisma","profanity-filter","security","sequelize","slug","tr39","typeorm","typescript","unicode","username","validation"],"created_at":"2026-04-13T07:47:34.699Z","updated_at":"2026-04-13T07:47:38.801Z","avatar_url":"https://github.com/paultendo.png","language":"TypeScript","funding_links":["https://github.com/sponsors/paultendo","https://buymeacoffee.com/paultendo"],"categories":[],"sub_categories":[],"readme":"# namespace-guard\n\n[![npm version](https://img.shields.io/npm/v/namespace-guard.svg)](https://www.npmjs.com/package/namespace-guard)\n[![bundle size](https://img.shields.io/bundlephobia/minzip/namespace-guard)](https://bundlephobia.com/package/namespace-guard)\n[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n**The world's first library that detects confusable characters across non-Latin scripts.** Slug claimability, Unicode anti-spoofing, and LLM [Denial of Spend](https://paultendo.github.io/posts/confusable-vision-llm-attack-tests/) defence in one zero-dependency package.\n\n- Live demo: https://paultendo.github.io/namespace-guard/\n- Blog post: https://paultendo.github.io/posts/namespace-guard-launch/\n\n## Cross-script confusable detection\n\nExisting confusable standards (TR39, IDNA) map non-Latin characters to Latin equivalents. They have zero coverage for confusable pairs *between* two non-Latin scripts.\n\nnamespace-guard ships 3,525 cross-script pairs from [confusable-vision](https://github.com/paultendo/confusable-vision) (measured across 245 system fonts using vector-outline raycasting — [RaySpace](https://paultendo.github.io/posts/rayspace-methodology/)). This catches attacks that no other library detects:\n\n```typescript\nimport { areConfusable, detectCrossScriptRisk } from \"namespace-guard\";\nimport { CONFUSABLE_WEIGHTS } from \"namespace-guard/confusable-weights\";\n\n// Hangul ᅵ and Han 丨 are visually identical (ray distance 0.004, Arial Unicode MS)\nareConfusable(\"\\u1175\", \"\\u4E28\", { weights: CONFUSABLE_WEIGHTS }); // true\n\n// Greek Τ and Han 丅 are near-identical (multiple fonts)\nareConfusable(\"\\u03A4\", \"\\u4E05\", { weights: CONFUSABLE_WEIGHTS }); // true\n\n// Cyrillic І and Greek Ι are identical outlines (62 fonts)\nareConfusable(\"\\u0406\", \"\\u0399\", { weights: CONFUSABLE_WEIGHTS }); // true\n\n// Without weights, only skeleton-based detection (TR39 coverage)\nareConfusable(\"\\u1175\", \"\\u4E28\"); // false\n\n// Analyze an identifier for cross-script risk\nconst risk = detectCrossScriptRisk(\"\\u1175\\u4E28\", { weights: CONFUSABLE_WEIGHTS });\n// { riskLevel: \"high\", scripts: [\"han\", \"hangul\"], crossScriptPairs: [...] }\n```\n\n4,174 total confusable pairs scored by visual measurement (3,111 TR39-confirmed, 1,063 novel). Each pair carries a `danger` score (0–1) representing geometric similarity across fonts; the shipped dataset uses a 0.5 floor. For higher precision, filter at `danger \u003e 0.7` (574 pairs). Cross-script data licensed CC-BY-4.0.\n\n## Installation\n\n```bash\nnpm install namespace-guard\n```\n\n## Quick Start (60 seconds)\n\n```typescript\nimport { createNamespaceGuardWithProfile } from \"namespace-guard\";\nimport { createPrismaAdapter } from \"namespace-guard/adapters/prisma\";\nimport { PrismaClient } from \"@prisma/client\";\n\nconst prisma = new PrismaClient();\n\nconst guard = createNamespaceGuardWithProfile(\n  \"consumer-handle\",\n  {\n    reserved: [\"admin\", \"api\", \"settings\", \"dashboard\", \"login\", \"signup\"],\n    sources: [\n      { name: \"user\", column: \"handleCanonical\", scopeKey: \"id\" },\n      { name: \"organization\", column: \"slugCanonical\", scopeKey: \"id\" },\n    ],\n  },\n  createPrismaAdapter(prisma)\n);\n\nawait guard.assertClaimable(\"acme-corp\");\n```\n\nFor race-safe writes, use `claim()`:\n\n```typescript\nconst result = await guard.claim(input.handle, async (canonical) =\u003e {\n  return prisma.user.create({\n    data: {\n      handle: input.handle,\n      handleCanonical: canonical,\n    },\n  });\n});\n\nif (!result.claimed) {\n  return { error: result.message };\n}\n```\n\n## What You Get\n\n- **Cross-script confusable detection** with 3,525 measured pairs between non-Latin scripts\n- Cross-table collision checks (users, orgs, teams, etc.)\n- Reserved-name blocking with category-aware messages\n- Unicode anti-spoofing (NFKC + confusable detection + mixed-script/risk controls)\n- Invisible character detection (zero-width joiners, direction overrides, and other hidden bytes)\n- Optional profanity/evasion validation\n- Suggestion strategies for taken names\n- CLI for red-team generation, calibration, drift, and CI gates\n\n## LLM Pipeline Preprocessing\n\nConfusable characters are pixel-identical to Latin letters but encode as multi-byte BPE tokens. A 95-line contract that costs 881 tokens in clean ASCII costs 4,567 tokens when flooded with confusables: **5.2x the API bill**. The model reads it correctly. The invoice does not care.\n\nWe tested this across 4 frontier models, 8 attack types, and 130+ API calls. Zero meaning flips. Every substituted clause was correctly interpreted. But the billing attack succeeds. We call it **Denial of Spend**: the confusable analogue of DDoS, where the attacker cannot degrade the service but can inflate the cost of running it.\n\n`canonicalise()` recovered every substituted term across all 12 attack variants, collapsing the 5.2x inflation to 1.0x. Processing a 10,000-character document takes under 1ms.\n\n```typescript\nimport { canonicalise, scan, isClean } from \"namespace-guard\";\n\nconst raw = \"The seller аssumes аll liаbility.\";\n\nconst report = scan(raw);        // detailed findings + risk level\nconst clean = canonicalise(raw); // \"The seller assumes all liability.\"\nconst ok = isClean(raw);         // false (mixed-script confusable detected)\n\n// For known-Latin documents (e.g. English contracts), use strategy: \"all\"\n// to also catch words where every character was substituted:\ncanonicalise(\"поп-refundable\", { strategy: \"all\" }); // \"non-refundable\"\n```\n\nResearch:\n- Denial of Spend: https://paultendo.github.io/posts/confusable-vision-llm-attack-tests/\n- Launch: https://paultendo.github.io/posts/namespace-guard-launch/\n- NFKC/TR39 composability: https://paultendo.github.io/posts/unicode-confusables-nfkc-conflict/\n\n## Advanced Security Primitives\n\nLow-level helpers for custom scoring, pairwise checks, and cross-script risk analysis:\n\n```typescript\nimport { skeleton, areConfusable, confusableDistance } from \"namespace-guard\";\n\nskeleton(\"pa\\u0443pal\"); // \"paypal\" skeleton form\nareConfusable(\"paypal\", \"pa\\u0443pal\"); // true\nconfusableDistance(\"paypal\", \"pa\\u0443pal\"); // graded similarity + chainDepth + explainable steps\n```\n\nFor measured visual scoring, pass the optional weights from confusable-vision (4,174 pairs scored across 245 fonts using vector-outline raycasting, including 3,525 cross-script pairs). Each pair has a `danger` score (0–1); the default 0.5 floor favours recall, use `danger \u003e 0.7` for precision. The `context` filter restricts to identifier-valid, domain-valid, or all pairs.\n\n```typescript\nimport { confusableDistance } from \"namespace-guard\";\nimport { CONFUSABLE_WEIGHTS } from \"namespace-guard/confusable-weights\";\n\nconst result = confusableDistance(\"paypal\", \"pa\\u0443pal\", {\n  weights: CONFUSABLE_WEIGHTS,\n  context: \"identifier\",\n});\n// result.similarity, result.steps (including \"visual-weight\" reason for novel pairs)\n```\n\n### Realistic Domain Spoof Detection\n\nFor domain name validation, `isDomainSpoof()` only flags threats that could produce registrable domain names. ICANN registrars enforce single-script labels, so mixed-script spoofs (e.g., one Cyrillic letter in a Latin domain) are excluded — they can't actually be registered.\n\n```typescript\nimport { isDomainSpoof } from \"namespace-guard\";\nimport { CONFUSABLE_WEIGHTS } from \"namespace-guard/confusable-weights\";\n\n// Full-Cyrillic lookalike — registrable and deceptive\nisDomainSpoof(\"\\u0440\\u0430\\u0443\\u0440\\u0430\\u04CF\", \"paypal\", { weights: CONFUSABLE_WEIGHTS });\n// { spoof: true, script: \"cyrillic\", danger: 0.91, substitutions: [...] }\n\n// Mixed-script — not registrable, not flagged\nisDomainSpoof(\"\\u0440aypal\", \"paypal\", { weights: CONFUSABLE_WEIGHTS });\n// { spoof: false }\n\n// Known-legitimate non-Latin domain — skip via allowlist\nisDomainSpoof(\"\\u0430\\u0441\\u0435\", \"ace\", {\n  weights: CONFUSABLE_WEIGHTS,\n  allowlist: [\"\\u0430\\u0441\\u0435\"],\n});\n// { spoof: false }\n```\n\nThe `danger` score (0–1) is always returned when a script match is found, even if below the `minDanger` threshold (default 0.5). Set `minDanger: 0.7` for higher precision.\n\n## Research\n\nTwo research tracks feed the library:\n\n**Visual measurement.** 4,174 confusable pairs measured across 245 system fonts using vector-outline raycasting ([RaySpace](https://paultendo.github.io/posts/rayspace-methodology/)). 3,525 of these are cross-script pairs between non-Latin scripts (Hangul/Han, Cyrillic/Greek, Cyrillic/Arabic, and more) with zero coverage in any existing standard. Each pair carries a `danger` score (0–1) representing geometric similarity; the shipped floor is 0.5 (for higher precision, try 0.7). Full dataset published as [confusable-vision](https://github.com/paultendo/confusable-vision) (CC-BY-4.0).\n\n**Normalisation composability.** 31 characters where Unicode's confusables.txt and NFKC normalisation disagree. Two production maps (`CONFUSABLE_MAP` for NFKC-first, `CONFUSABLE_MAP_FULL` for raw-input pipelines), a benchmark corpus, and composability vectors wired into CLI drift baselines. Submitted to [Unicode public review (PRI #540)](https://www.unicode.org/review/pri540/) and published in [accumulated feedback](https://www.unicode.org/review/pri540/feedback.html).\n\n- Technical reference: [docs/reference.md#how-the-anti-spoofing-pipeline-works](docs/reference.md#how-the-anti-spoofing-pipeline-works)\n- Launch write-up: https://paultendo.github.io/posts/namespace-guard-launch/\n- Denial of Spend: https://paultendo.github.io/posts/confusable-vision-llm-attack-tests/\n\n## Built-in Profiles\n\nUse `createNamespaceGuardWithProfile(profile, overrides, adapter)`:\n\n- `consumer-handle`: strict defaults for public handles\n- `org-slug`: workspace/org slugs\n- `developer-id`: technical IDs with looser numeric rules\n\nProfiles are defaults, not lock-in. Override only what you need.\n\n## Zero-Dependency Moderation Integration\n\nCore stays zero-dependency. You can use built-ins or plug in any external library.\n\n```typescript\nimport {\n  createNamespaceGuard,\n  createPredicateValidator,\n} from \"namespace-guard\";\nimport { createEnglishProfanityValidator } from \"namespace-guard/profanity-en\";\n\nconst guard = createNamespaceGuard(\n  {\n    sources: [\n      { name: \"user\", column: \"handleCanonical\", scopeKey: \"id\" },\n      { name: \"organization\", column: \"slugCanonical\", scopeKey: \"id\" },\n    ],\n    validators: [\n      createEnglishProfanityValidator({ mode: \"evasion\" }),\n      createPredicateValidator((identifier) =\u003e thirdPartyFilter.has(identifier)),\n    ],\n  },\n  adapter\n);\n```\n\n## CLI Workflow\n\n```bash\n# 1) Generate realistic attack variants\nnpx namespace-guard attack-gen paypal --json\n\n# 2) Calibrate thresholds and CI gate suggestions from your dataset\nnpx namespace-guard recommend ./risk-dataset.json\n\n# 3) Preflight canonical collisions before adding DB unique constraints\nnpx namespace-guard audit-canonical ./users-export.json --json\n\n# 4) Compare TR39-full vs NFKC-filtered behaviour\nnpx namespace-guard drift --json\n```\n\n## Adapter Support\n\n- Prisma\n- Drizzle\n- Kysely\n- Knex\n- TypeORM\n- MikroORM\n- Sequelize\n- Mongoose\n- Raw SQL\n\nAdapter setup examples and migration guidance: [docs/reference.md#adapters](docs/reference.md#adapters)\n\n## Production Recommendation: Canonical Uniqueness\n\nFor full protection against Unicode/canonicalization edge cases, enforce uniqueness on canonical columns (for example `handleCanonical`, `slugCanonical`) and point `sources[*].column` there.\n\nMigration guides per adapter: [docs/reference.md#canonical-uniqueness-migration-per-adapter](docs/reference.md#canonical-uniqueness-migration-per-adapter)\n\n## Documentation Map\n\n- Full reference: [docs/reference.md](docs/reference.md)\n- Config reference: [docs/reference.md#configuration](docs/reference.md#configuration)\n- Validators (profanity, homoglyph, invisible): [docs/reference.md#async-validators](docs/reference.md#async-validators)\n- Canonical preflight audit (`audit-canonical`): [docs/reference.md#audit-canonical-command](docs/reference.md#audit-canonical-command)\n- Anti-spoofing pipeline and composability vectors: [docs/reference.md#how-the-anti-spoofing-pipeline-works](docs/reference.md#how-the-anti-spoofing-pipeline-works)\n- LLM preprocessing (`canonicalise`, `scan`, `isClean`): [docs/reference.md#llm-pipeline-preprocessing](docs/reference.md#llm-pipeline-preprocessing)\n- Benchmark corpus (`confusable-bench.v1`): [docs/reference.md#confusable-benchmark-corpus-artifact](docs/reference.md#confusable-benchmark-corpus-artifact)\n- Advanced primitives (`skeleton`, `areConfusable`, `confusableDistance`): [docs/reference.md#advanced-security-primitives](docs/reference.md#advanced-security-primitives)\n- Confusable weights (scored pairs, including cross-script): [docs/reference.md#confusable-weights-subpath](docs/reference.md#confusable-weights-subpath)\n- Cross-script detection: [docs/reference.md#cross-script-detection](docs/reference.md#cross-script-detection)\n- CLI reference: [docs/reference.md#cli](docs/reference.md#cli)\n- API reference: [docs/reference.md#api-reference](docs/reference.md#api-reference)\n- Framework integration (Next.js/Express/tRPC): [docs/reference.md#framework-integration](docs/reference.md#framework-integration)\n\n## Support\n\nIf `namespace-guard` helped you, please star the repo. It helps the project a lot.\n\n- GitHub Sponsors: https://github.com/sponsors/paultendo\n- Buy me a coffee: https://buymeacoffee.com/paultendo\n\n## Contributing\n\nContributions welcome. Please open an issue first to discuss larger changes.\n\n## License\n\nMIT © [Paul Wood FRSA (@paultendo)](https://github.com/paultendo)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpaultendo%2Fnamespace-guard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpaultendo%2Fnamespace-guard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpaultendo%2Fnamespace-guard/lists"}