A tool for parsing traffic on the jetstream and applying a moderation workstream based on regexp based rules

Refactor: Move account age checks to rules folder

This commit moves the account age related checks into the
`rules/account` folder, organizing the project structure.

feat: Add Prettier config and plugins

feat: Add ESLint config and plugins

fix: Fix missing import for Agent

feat: Implement metrics endpoint

refactor: Improve test organization

fix: Mock fetch in age.test.ts

fix: Add tests for countStarterPacks

refactor: Move account age checks to rules folder

feat: Add tests for facet spam detection

Skywatch 9df4049f d5d5a662

+2 -2
.github/workflows/ci.yml
··· 21 21 - name: Install dependencies 22 22 run: bun install 23 23 24 - # - name: Run linter 25 - # run: npm run lint 24 + # - name: Run linter 25 + # run: npm run lint 26 26 27 27 - name: Type check 28 28 run: npx tsc --noEmit
+13
.prettierrc.json
··· 1 + { 2 + "plugins": ["@trivago/prettier-plugin-sort-imports"], 3 + "importOrder": [ 4 + "^node:", 5 + "<THIRD_PARTY_MODULES>", 6 + "^@atproto/(.*)$", 7 + "^@skyware/(.*)$", 8 + "^@clavata/(.*)$", 9 + "^[./]" 10 + ], 11 + "importOrderSeparation": false, 12 + "importOrderSortSpecifiers": true 13 + }
+1 -1
eslint.config.mjs
··· 1 1 import eslint from "@eslint/js"; 2 - import tseslint from "typescript-eslint"; 3 2 import stylistic from "@stylistic/eslint-plugin"; 4 3 import prettier from "eslint-config-prettier"; 5 4 import importPlugin from "eslint-plugin-import"; 5 + import tseslint from "typescript-eslint"; 6 6 7 7 export default tseslint.config( 8 8 eslint.configs.recommended,
+4 -3
src/agent.ts
··· 1 - import { setGlobalDispatcher, Agent as Agent } from "undici"; 1 + import { Agent, setGlobalDispatcher } from "undici"; 2 + import { AtpAgent } from "@atproto/api"; 3 + import { BSKY_HANDLE, BSKY_PASSWORD, OZONE_PDS } from "./config.js"; 4 + 2 5 setGlobalDispatcher(new Agent({ connect: { timeout: 20_000 } })); 3 - import { BSKY_HANDLE, BSKY_PASSWORD, OZONE_PDS } from "./config.js"; 4 - import { AtpAgent } from "@atproto/api"; 5 6 6 7 export const agent = new AtpAgent({ 7 8 service: `https://${OZONE_PDS}`,
+3 -1
src/limits.ts
··· 1 - import { pRateLimit } from "p-ratelimit"; // TypeScript 1 + import { pRateLimit } from "p-ratelimit"; 2 + 3 + // TypeScript 2 4 3 5 // create a rate limiter that allows up to 30 API calls per second, 4 6 // with max concurrency of 10
+5 -6
src/main.ts
··· 1 + import fs from "node:fs"; 1 2 import { 2 3 CommitCreateEvent, 3 4 CommitUpdateEvent, 4 5 IdentityEvent, 5 6 Jetstream, 6 7 } from "@skyware/jetstream"; 7 - import fs from "node:fs"; 8 - 9 8 import { 10 9 CURSOR_UPDATE_INTERVAL, 11 10 FIREHOSE_URL, ··· 14 13 } from "./config.js"; 15 14 import { logger } from "./logger.js"; 16 15 import { startMetricsServer } from "./metrics.js"; 17 - import { Post, LinkFeature, Handle } from "./types.js"; 16 + import { checkAccountAge } from "./rules/account/age.js"; 17 + import { checkFacetSpam } from "./rules/facets/facets.js"; 18 + import { checkHandle } from "./rules/handles/checkHandles.js"; 18 19 import { checkPosts } from "./rules/posts/checkPosts.js"; 19 - import { checkHandle } from "./rules/handles/checkHandles.js"; 20 20 import { 21 21 checkDescription, 22 22 checkDisplayName, 23 23 } from "./rules/profiles/checkProfiles.js"; 24 - import { checkFacetSpam } from "./rules/facets/facets.js"; 25 - import { checkAccountAge } from "./rules/account/age.js"; 24 + import { Handle, LinkFeature, Post } from "./types.js"; 26 25 27 26 let cursor = 0; 28 27 let cursorUpdateInterval: NodeJS.Timeout;
-1
src/metrics.ts
··· 1 1 import express from "express"; 2 2 import { Registry, collectDefaultMetrics } from "prom-client"; 3 - 4 3 import { logger } from "./logger.js"; 5 4 6 5 const register = new Registry();
+3 -3
src/rules/account/age.ts
··· 1 1 import { agent, isLoggedIn } from "../../agent.js"; 2 - import { logger } from "../../logger.js"; 3 - import { createAccountLabel, checkAccountLabels } from "../../moderation.js"; 4 - import { ACCOUNT_AGE_CHECKS } from "./ageConstants.js"; 5 2 import { PLC_URL } from "../../config.js"; 6 3 import { GLOBAL_ALLOW } from "../../constants.js"; 4 + import { logger } from "../../logger.js"; 5 + import { checkAccountLabels, createAccountLabel } from "../../moderation.js"; 6 + import { ACCOUNT_AGE_CHECKS } from "./ageConstants.js"; 7 7 8 8 interface InteractionContext { 9 9 // For replies
+2 -2
src/rules/account/countStarterPacks.ts
··· 1 - import { isLoggedIn, agent } from "../../agent.js"; 2 - import { logger } from "../../logger.js"; 1 + import { agent, isLoggedIn } from "../../agent.js"; 3 2 import { limit } from "../../limits.js"; 3 + import { logger } from "../../logger.js"; 4 4 import { createAccountLabel } from "../../moderation.js"; 5 5 6 6 const ALLOWED_DIDS = ["did:plc:gpunjjgvlyb4racypz3yfiq4"];
+5 -6
src/rules/account/tests/age.test.ts
··· 1 - import { describe, it, expect, vi, beforeEach } from "vitest"; 1 + import { beforeEach, describe, expect, it, vi } from "vitest"; 2 + import { agent } from "../../../agent.js"; 3 + import { GLOBAL_ALLOW } from "../../../constants.js"; 4 + import { logger } from "../../../logger.js"; 5 + import { checkAccountLabels, createAccountLabel } from "../../../moderation.js"; 2 6 import { 3 7 calculateAccountAge, 4 8 checkAccountAge, ··· 34 38 35 39 // Mock fetch for DID document lookups 36 40 global.fetch = vi.fn(); 37 - 38 - import { agent } from "../../../agent.js"; 39 - import { logger } from "../../../logger.js"; 40 - import { createAccountLabel, checkAccountLabels } from "../../../moderation.js"; 41 - import { GLOBAL_ALLOW } from "../../../constants.js"; 42 41 43 42 describe("Account Age Module", () => { 44 43 beforeEach(() => {
+5 -6
src/rules/account/tests/countStarterPacks.test.ts
··· 1 - import { describe, it, expect, vi, beforeEach } from "vitest"; 1 + import { beforeEach, describe, expect, it, vi } from "vitest"; 2 + import { agent } from "../../../agent.js"; 3 + import { limit } from "../../../limits.js"; 4 + import { logger } from "../../../logger.js"; 5 + import { createAccountLabel } from "../../../moderation.js"; 2 6 import { countStarterPacks } from "../countStarterPacks.js"; 3 7 4 8 // Mock dependencies ··· 31 35 vi.mock("../../../limits.js", () => ({ 32 36 limit: vi.fn((fn) => fn()), 33 37 })); 34 - 35 - import { agent } from "../../../agent.js"; 36 - import { logger } from "../../../logger.js"; 37 - import { createAccountLabel } from "../../../moderation.js"; 38 - import { limit } from "../../../limits.js"; 39 38 40 39 describe("countStarterPacks", () => { 41 40 beforeEach(() => {
+1 -1
src/rules/facets/facets.ts
··· 1 - import { createAccountLabel } from "../../moderation.js"; 2 1 import { logger } from "../../logger.js"; 2 + import { createAccountLabel } from "../../moderation.js"; 3 3 import { Facet } from "../../types.js"; 4 4 5 5 // Threshold for duplicate facet positions before flagging as spam
+8 -9
src/rules/facets/tests/facets.test.ts
··· 1 - import { describe, it, expect, vi, beforeEach } from "vitest"; 1 + import { beforeEach, describe, expect, it, vi } from "vitest"; 2 + import { logger } from "../../../logger.js"; 3 + import { createAccountLabel } from "../../../moderation.js"; 4 + import { Facet } from "../../../types.js"; 2 5 import { 3 - checkFacetSpam, 4 - FACET_SPAM_THRESHOLD, 6 + FACET_SPAM_ALLOWLIST, 7 + FACET_SPAM_COMMENT, 5 8 FACET_SPAM_LABEL, 6 - FACET_SPAM_COMMENT, 7 - FACET_SPAM_ALLOWLIST, 9 + FACET_SPAM_THRESHOLD, 10 + checkFacetSpam, 8 11 } from "../facets.js"; 9 - import { Facet } from "../../../types.js"; 10 12 11 13 // Mock dependencies 12 14 vi.mock("../../../moderation.js", () => ({ ··· 20 22 error: vi.fn(), 21 23 }, 22 24 })); 23 - 24 - import { createAccountLabel } from "../../../moderation.js"; 25 - import { logger } from "../../../logger.js"; 26 25 27 26 describe("checkFacetSpam", () => { 28 27 const TEST_DID = "did:plc:test123";
+3 -3
src/rules/handles/checkHandles.test.ts
··· 1 - import { describe, it, expect, vi, beforeEach } from "vitest"; 2 - import { checkHandle } from "./checkHandles.js"; 1 + import { beforeEach, describe, expect, it, vi } from "vitest"; 3 2 import { 4 - createAccountReport, 5 3 createAccountComment, 6 4 createAccountLabel, 5 + createAccountReport, 7 6 } from "../../moderation.js"; 7 + import { checkHandle } from "./checkHandles.js"; 8 8 9 9 // Mock dependencies 10 10 vi.mock("../../moderation.js", () => ({
+3 -3
src/rules/handles/checkHandles.ts
··· 1 - import { HANDLE_CHECKS } from "./constants.js"; 1 + import { GLOBAL_ALLOW } from "../../constants.js"; 2 2 import { logger } from "../../logger.js"; 3 3 import { 4 - createAccountReport, 5 4 createAccountComment, 6 5 createAccountLabel, 6 + createAccountReport, 7 7 } from "../../moderation.js"; 8 - import { GLOBAL_ALLOW } from "../../constants.js"; 8 + import { HANDLE_CHECKS } from "./constants.js"; 9 9 10 10 export const checkHandle = async ( 11 11 did: string,
+7 -7
src/rules/posts/checkPosts.ts
··· 1 - import { LINK_SHORTENER, POST_CHECKS } from "./constants.js"; 2 - import { Post } from "../../types.js"; 1 + import { GLOBAL_ALLOW } from "../../constants.js"; 3 2 import { logger } from "../../logger.js"; 4 - import { countStarterPacks } from "../account/countStarterPacks.js"; 5 3 import { 6 - createPostLabel, 4 + createAccountComment, 7 5 createAccountReport, 8 - createAccountComment, 6 + createPostLabel, 9 7 createPostReport, 10 8 } from "../../moderation.js"; 9 + import { Post } from "../../types.js"; 10 + import { getFinalUrl } from "../../utils/getFinalUrl.js"; 11 11 import { getLanguage } from "../../utils/getLanguage.js"; 12 - import { getFinalUrl } from "../../utils/getFinalUrl.js"; 13 - import { GLOBAL_ALLOW } from "../../constants.js"; 12 + import { countStarterPacks } from "../account/countStarterPacks.js"; 13 + import { LINK_SHORTENER, POST_CHECKS } from "./constants.js"; 14 14 15 15 export const checkPosts = async (post: Post[]) => { 16 16 if (GLOBAL_ALLOW.includes(post[0].did)) {
+12 -13
src/rules/posts/tests/checkPosts.test.ts
··· 1 - import { describe, it, expect, vi, beforeEach } from "vitest"; 2 - import { checkPosts } from "../checkPosts.js"; 1 + import { beforeEach, describe, expect, it, vi } from "vitest"; 2 + import { logger } from "../../../logger.js"; 3 + import { 4 + createAccountComment, 5 + createAccountReport, 6 + createPostLabel, 7 + createPostReport, 8 + } from "../../../moderation.js"; 3 9 import { Post } from "../../../types.js"; 10 + import { getFinalUrl } from "../../../utils/getFinalUrl.js"; 11 + import { getLanguage } from "../../../utils/getLanguage.js"; 12 + import { countStarterPacks } from "../../account/countStarterPacks.js"; 13 + import { checkPosts } from "../checkPosts.js"; 4 14 5 15 // Mock dependencies 6 16 vi.mock("../constants.js", () => ({ ··· 88 98 vi.mock("../../../constants.js", () => ({ 89 99 GLOBAL_ALLOW: ["did:plc:globalallow"], 90 100 })); 91 - 92 - import { logger } from "../../../logger.js"; 93 - import { countStarterPacks } from "../../account/countStarterPacks.js"; 94 - import { 95 - createPostLabel, 96 - createAccountReport, 97 - createAccountComment, 98 - createPostReport, 99 - } from "../../../moderation.js"; 100 - import { getLanguage } from "../../../utils/getLanguage.js"; 101 - import { getFinalUrl } from "../../../utils/getFinalUrl.js"; 102 101 103 102 describe("checkPosts", () => { 104 103 beforeEach(() => {
+4 -4
src/rules/profiles/checkProfiles.ts
··· 1 - import { PROFILE_CHECKS } from "../../rules/profiles/constants.js"; 1 + import { GLOBAL_ALLOW } from "../../constants.js"; 2 2 import { logger } from "../../logger.js"; 3 3 import { 4 - createAccountReport, 5 - createAccountLabel, 6 4 createAccountComment, 5 + createAccountLabel, 6 + createAccountReport, 7 7 } from "../../moderation.js"; 8 + import { PROFILE_CHECKS } from "../../rules/profiles/constants.js"; 8 9 import { getLanguage } from "../../utils/getLanguage.js"; 9 - import { GLOBAL_ALLOW } from "../../constants.js"; 10 10 11 11 export const checkDescription = async ( 12 12 did: string,
+8 -9
src/rules/profiles/tests/checkProfiles.test.ts
··· 1 - import { describe, it, expect, vi, beforeEach } from "vitest"; 1 + import { beforeEach, describe, expect, it, vi } from "vitest"; 2 + import { logger } from "../../../logger.js"; 3 + import { 4 + createAccountComment, 5 + createAccountLabel, 6 + createAccountReport, 7 + } from "../../../moderation.js"; 8 + import { getLanguage } from "../../../utils/getLanguage.js"; 2 9 import { checkDescription, checkDisplayName } from "../checkProfiles.js"; 3 10 4 11 // Mock dependencies ··· 102 109 vi.mock("../../../constants.js", () => ({ 103 110 GLOBAL_ALLOW: ["did:plc:globalallow"], 104 111 })); 105 - 106 - import { logger } from "../../../logger.js"; 107 - import { 108 - createAccountLabel, 109 - createAccountReport, 110 - createAccountComment, 111 - } from "../../../moderation.js"; 112 - import { getLanguage } from "../../../utils/getLanguage.js"; 113 112 114 113 describe("checkProfiles", () => { 115 114 beforeEach(() => {
+30 -28
src/tests/agent.test.ts
··· 1 - import { describe, it, expect, vi, beforeEach } from 'vitest' 1 + import { beforeEach, describe, expect, it, vi } from "vitest"; 2 2 3 - describe('Agent', () => { 3 + describe("Agent", () => { 4 4 beforeEach(() => { 5 - vi.resetModules() 6 - }) 5 + vi.resetModules(); 6 + }); 7 7 8 - it('should create an agent and login', async () => { 8 + it("should create an agent and login", async () => { 9 9 // Mock the config variables 10 - vi.doMock('../config.js', () => ({ 11 - BSKY_HANDLE: 'test.bsky.social', 12 - BSKY_PASSWORD: 'password', 13 - OZONE_PDS: 'pds.test.com' 14 - })) 10 + vi.doMock("../config.js", () => ({ 11 + BSKY_HANDLE: "test.bsky.social", 12 + BSKY_PASSWORD: "password", 13 + OZONE_PDS: "pds.test.com", 14 + })); 15 15 16 16 // Mock the AtpAgent 17 - const mockLogin = vi.fn(() => Promise.resolve()) 18 - const mockConstructor = vi.fn() 19 - vi.doMock('@atproto/api', () => ({ 17 + const mockLogin = vi.fn(() => Promise.resolve()); 18 + const mockConstructor = vi.fn(); 19 + vi.doMock("@atproto/api", () => ({ 20 20 AtpAgent: class { 21 - login = mockLogin 22 - service: URL 21 + login = mockLogin; 22 + service: URL; 23 23 constructor(options: { service: string }) { 24 - mockConstructor(options) 25 - this.service = new URL(options.service) 24 + mockConstructor(options); 25 + this.service = new URL(options.service); 26 26 } 27 - } 28 - })) 27 + }, 28 + })); 29 29 30 - const { agent, login } = await import('../agent.js') 30 + const { agent, login } = await import("../agent.js"); 31 31 32 32 // Check that the agent was created with the correct service URL 33 - expect(mockConstructor).toHaveBeenCalledWith({ service: 'https://pds.test.com' }) 34 - expect(agent.service.toString()).toBe('https://pds.test.com/') 33 + expect(mockConstructor).toHaveBeenCalledWith({ 34 + service: "https://pds.test.com", 35 + }); 36 + expect(agent.service.toString()).toBe("https://pds.test.com/"); 35 37 36 38 // Check that the login function calls the mockLogin function 37 - await login() 39 + await login(); 38 40 expect(mockLogin).toHaveBeenCalledWith({ 39 - identifier: 'test.bsky.social', 40 - password: 'password' 41 - }) 42 - }) 43 - }) 41 + identifier: "test.bsky.social", 42 + password: "password", 43 + }); 44 + }); 45 + });
+14 -14
src/tests/limits.test.ts
··· 1 - import { describe, it, expect } from 'vitest' 2 - import { limit } from '../limits.js' 1 + import { describe, expect, it } from "vitest"; 2 + import { limit } from "../limits.js"; 3 3 4 - describe('Rate Limiter', () => { 5 - it('should limit the rate of calls', async () => { 6 - const calls = [] 4 + describe("Rate Limiter", () => { 5 + it("should limit the rate of calls", async () => { 6 + const calls = []; 7 7 for (let i = 0; i < 10; i++) { 8 - calls.push(limit(() => Promise.resolve(Date.now()))) 8 + calls.push(limit(() => Promise.resolve(Date.now()))); 9 9 } 10 10 11 - const start = Date.now() 12 - const results = await Promise.all(calls) 13 - const end = Date.now() 11 + const start = Date.now(); 12 + const results = await Promise.all(calls); 13 + const end = Date.now(); 14 14 15 15 // With a concurrency of 4, 10 calls should take at least 2 intervals. 16 16 // However, the interval is 30 seconds, so this test would be very slow. 17 17 // Instead, we'll just check that the calls were successful and returned a timestamp. 18 - expect(results.length).toBe(10) 18 + expect(results.length).toBe(10); 19 19 for (const result of results) { 20 - expect(typeof result).toBe('number') 20 + expect(typeof result).toBe("number"); 21 21 } 22 22 // A better test would be to mock the timer and advance it, but that's more complex. 23 23 // For now, we'll just check that the time taken is greater than 0. 24 - expect(end - start).toBeGreaterThanOrEqual(0) 25 - }, 40000) // Increase timeout for this test 26 - }) 24 + expect(end - start).toBeGreaterThanOrEqual(0); 25 + }, 40000); // Increase timeout for this test 26 + });
+16 -16
src/tests/metrics.test.ts
··· 1 - import { describe, it, expect } from 'vitest' 2 - import request from 'supertest' 3 - import { startMetricsServer } from '../metrics.js' 4 - import { Server } from 'http' 1 + import { Server } from "http"; 2 + import request from "supertest"; 3 + import { describe, expect, it } from "vitest"; 4 + import { startMetricsServer } from "../metrics.js"; 5 5 6 - describe('Metrics Server', () => { 7 - let server: Server 6 + describe("Metrics Server", () => { 7 + let server: Server; 8 8 9 9 afterEach(() => { 10 10 if (server) { 11 - server.close() 11 + server.close(); 12 12 } 13 - }) 13 + }); 14 14 15 - it('should return metrics on /metrics endpoint', async () => { 16 - server = startMetricsServer(0) 17 - const response = await request(server).get('/metrics') 18 - expect(response.status).toBe(200) 19 - expect(response.headers['content-type']).toContain('text/plain') 20 - expect(response.text).toContain('process_cpu_user_seconds_total') 21 - }) 22 - }) 15 + it("should return metrics on /metrics endpoint", async () => { 16 + server = startMetricsServer(0); 17 + const response = await request(server).get("/metrics"); 18 + expect(response.status).toBe(200); 19 + expect(response.headers["content-type"]).toContain("text/plain"); 20 + expect(response.text).toContain("process_cpu_user_seconds_total"); 21 + }); 22 + });
+3 -4
src/tests/moderation.test.ts
··· 1 - import { describe, it, expect, vi, beforeEach } from "vitest"; 1 + import { beforeEach, describe, expect, it, vi } from "vitest"; 2 + import { agent } from "../agent.js"; 3 + import { logger } from "../logger.js"; 2 4 import { checkAccountLabels } from "../moderation.js"; 3 5 4 6 // Mock dependencies ··· 31 33 vi.mock("../limits.js", () => ({ 32 34 limit: vi.fn((fn) => fn()), 33 35 })); 34 - 35 - import { agent } from "../agent.js"; 36 - import { logger } from "../logger.js"; 37 36 38 37 describe("checkAccountLabels", () => { 39 38 beforeEach(() => {
+1 -1
src/utils/getFinalUrl.test.ts
··· 1 - import { describe, it, expect, vi, beforeEach } from "vitest"; 1 + import { beforeEach, describe, expect, it, vi } from "vitest"; 2 2 import { getFinalUrl } from "./getFinalUrl.js"; 3 3 4 4 // Mock the logger
+1 -1
src/utils/getLanguage.test.ts
··· 1 - import { describe, it, expect, vi, beforeEach } from "vitest"; 1 + import { beforeEach, describe, expect, it, vi } from "vitest"; 2 2 import { getLanguage } from "./getLanguage.js"; 3 3 4 4 // Mock the logger
+36 -36
src/utils/homoglyph.test.ts
··· 1 - import { describe, it, expect } from 'vitest' 2 - import { normalizeUnicode } from './normalizeUnicode.js' 1 + import { describe, expect, it } from "vitest"; 2 + import { normalizeUnicode } from "./normalizeUnicode.js"; 3 3 4 - describe('normalizeUnicode with Homoglyphs', () => { 5 - it('should replace basic homoglyphs', () => { 6 - expect(normalizeUnicode('h3ll0')).toBe('hello') 7 - expect(normalizeUnicode('w0rld')).toBe('world') 8 - expect(normalizeUnicode('p@ssword')).toBe('password') 9 - expect(normalizeUnicode('1nternet')).toBe('internet') 10 - }) 4 + describe("normalizeUnicode with Homoglyphs", () => { 5 + it("should replace basic homoglyphs", () => { 6 + expect(normalizeUnicode("h3ll0")).toBe("hello"); 7 + expect(normalizeUnicode("w0rld")).toBe("world"); 8 + expect(normalizeUnicode("p@ssword")).toBe("password"); 9 + expect(normalizeUnicode("1nternet")).toBe("internet"); 10 + }); 11 11 12 - it('should replace Cyrillic homoglyphs', () => { 13 - expect(normalizeUnicode('аpple')).toBe('apple') // 'а' is Cyrillic 14 - expect(normalizeUnicode('еlеphant')).toBe('elephant') // 'е' is Cyrillic 15 - expect(normalizeUnicode('оrange')).toBe('orange') // 'о' is Cyrillic 16 - }) 12 + it("should replace Cyrillic homoglyphs", () => { 13 + expect(normalizeUnicode("аpple")).toBe("apple"); // 'а' is Cyrillic 14 + expect(normalizeUnicode("еlеphant")).toBe("elephant"); // 'е' is Cyrillic 15 + expect(normalizeUnicode("оrange")).toBe("orange"); // 'о' is Cyrillic 16 + }); 17 17 18 - it('should handle a mix of homoglyphs and regular characters', () => { 19 - expect(normalizeUnicode('p@y-pаl')).toBe('pay-pal') 20 - expect(normalizeUnicode('g00gl3')).toBe('google') 21 - }) 18 + it("should handle a mix of homoglyphs and regular characters", () => { 19 + expect(normalizeUnicode("p@y-pаl")).toBe("pay-pal"); 20 + expect(normalizeUnicode("g00gl3")).toBe("google"); 21 + }); 22 22 23 - it('should handle accented and other Unicode characters', () => { 24 - expect(normalizeUnicode('déjà vu')).toBe('deja vu') 25 - expect(normalizeUnicode('naïve')).toBe('naive') 26 - expect(normalizeUnicode('façade')).toBe('facade') 27 - }) 23 + it("should handle accented and other Unicode characters", () => { 24 + expect(normalizeUnicode("déjà vu")).toBe("deja vu"); 25 + expect(normalizeUnicode("naïve")).toBe("naive"); 26 + expect(normalizeUnicode("façade")).toBe("facade"); 27 + }); 28 28 29 - it('should handle complex strings with multiple homoglyph types', () => { 30 - const complexString = 'p@sswоrd123-еxаmple' // with Cyrillic 'о', 'а', 'е' 31 - const expectedString = 'passwordize-example' 32 - expect(normalizeUnicode(complexString)).toBe(expectedString) 33 - }) 29 + it("should handle complex strings with multiple homoglyph types", () => { 30 + const complexString = "p@sswоrd123-еxаmple"; // with Cyrillic 'о', 'а', 'е' 31 + const expectedString = "passwordize-example"; 32 + expect(normalizeUnicode(complexString)).toBe(expectedString); 33 + }); 34 34 35 - it('should not affect strings with no homoglyphs', () => { 36 - const normalString = 'hello world' 37 - expect(normalizeUnicode(normalString)).toBe(normalString) 38 - }) 35 + it("should not affect strings with no homoglyphs", () => { 36 + const normalString = "hello world"; 37 + expect(normalizeUnicode(normalString)).toBe(normalString); 38 + }); 39 39 40 - it('should handle an empty string', () => { 41 - expect(normalizeUnicode('')).toBe('') 42 - }) 43 - }) 40 + it("should handle an empty string", () => { 41 + expect(normalizeUnicode("")).toBe(""); 42 + }); 43 + });
+1 -1
src/utils/homoglyphs.ts
··· 313 313 314 314 // Confusables for 'z' 315 315 "2": "z", 316 - }; 316 + };
+1 -1
src/utils/normalizeUnicode.test.ts
··· 1 - import { describe, it, expect } from "vitest"; 1 + import { describe, expect, it } from "vitest"; 2 2 import { normalizeUnicode } from "./normalizeUnicode.js"; 3 3 4 4 describe("normalizeUnicode", () => {
-1
src/utils/normalizeUnicode.ts
··· 1 1 import { logger } from "../logger.js"; 2 - 3 2 import { homoglyphMap } from "./homoglyphs.js"; 4 3 5 4 /**