From 6167490dba546e878b9329357fa40e7e0d658067 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Mon, 23 Mar 2026 20:40:56 +0200 Subject: [PATCH 1/8] refactor(schemas): centralize regex constants --- frontend/src/ts/utils/misc.ts | 3 ++- packages/schemas/src/ape-keys.ts | 4 ++-- packages/schemas/src/presets.ts | 4 ++-- packages/schemas/src/users.ts | 14 +++++++------- packages/schemas/src/util.ts | 3 +++ 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index 631ffe597dd1..0c2b4e5ac0ce 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -3,6 +3,7 @@ import { Config } from "@monkeytype/schemas/configs"; import { Mode, Mode2, PersonalBests } from "@monkeytype/schemas/shared"; import { Result } from "@monkeytype/schemas/results"; import { RankAndCount } from "@monkeytype/schemas/users"; +import { SLUG_REGEX } from "@monkeytype/schemas/util"; import { roundTo2 } from "@monkeytype/util/numbers"; import { animate, AnimationParams } from "animejs"; import { ElementWithUtils } from "./dom"; @@ -151,7 +152,7 @@ export function isUsernameValid(name: string): boolean { if (name.toLowerCase().includes("bitly")) return false; if (name.length > 14) return false; if (/^\..*/.test(name.toLowerCase())) return false; - return /^[0-9a-zA-Z_.-]+$/.test(name); + return SLUG_REGEX.test(name); } export function clearTimeouts(timeouts: (number | NodeJS.Timeout)[]): void { diff --git a/packages/schemas/src/ape-keys.ts b/packages/schemas/src/ape-keys.ts index 146c4cc658a1..21a29aa7bb6b 100644 --- a/packages/schemas/src/ape-keys.ts +++ b/packages/schemas/src/ape-keys.ts @@ -1,9 +1,9 @@ import { z } from "zod"; -import { IdSchema } from "./util"; +import { IdSchema, SLUG_REGEX } from "./util"; export const ApeKeyNameSchema = z .string() - .regex(/^[0-9a-zA-Z_.-]+$/) + .regex(SLUG_REGEX) .max(20); export const ApeKeyUserDefinedSchema = z.object({ diff --git a/packages/schemas/src/presets.ts b/packages/schemas/src/presets.ts index 5faeb2a68224..fc8dd5414bd7 100644 --- a/packages/schemas/src/presets.ts +++ b/packages/schemas/src/presets.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { IdSchema, TagSchema } from "./util"; +import { IdSchema, NAME_REGEX, TagSchema } from "./util"; import { ConfigGroupName, ConfigGroupNameSchema, @@ -8,7 +8,7 @@ import { export const PresetNameSchema = z .string() - .regex(/^[0-9a-zA-Z_-]+$/) + .regex(NAME_REGEX) .max(16); export type PresetName = z.infer; diff --git a/packages/schemas/src/users.ts b/packages/schemas/src/users.ts index e592c1bea026..66238489baa1 100644 --- a/packages/schemas/src/users.ts +++ b/packages/schemas/src/users.ts @@ -1,5 +1,5 @@ import { z, ZodEffects, ZodOptional, ZodString } from "zod"; -import { IdSchema, StringNumberSchema } from "./util"; +import { IdSchema, NAME_REGEX, SLUG_REGEX, StringNumberSchema } from "./util"; import { LanguageSchema } from "./languages"; import { ModeSchema, @@ -20,7 +20,7 @@ export const ResultFiltersSchema = z.object({ _id: IdSchema, name: z .string() - .regex(/^[0-9a-zA-Z_.-]+$/) + .regex(NAME_REGEX) .max(16), pb: z .object({ @@ -94,14 +94,14 @@ export const TwitterProfileSchema = profileDetailsBase( z .string() .max(20) - .regex(/^[0-9a-zA-Z_.-]+$/), + .regex(SLUG_REGEX), ).or(z.literal("")); export const GithubProfileSchema = profileDetailsBase( z .string() .max(39) - .regex(/^[0-9a-zA-Z_.-]+$/), + .regex(SLUG_REGEX), ).or(z.literal("")); export const WebsiteSchema = profileDetailsBase( @@ -127,7 +127,7 @@ export type UserProfileDetails = z.infer; export const CustomThemeNameSchema = z .string() - .regex(/^[0-9a-zA-Z_-]+$/) + .regex(NAME_REGEX) .max(16); export type CustomThemeName = z.infer; @@ -249,7 +249,7 @@ export const UserNameSchema = doesNotContainProfanity( .min(1) .max(16) .regex( - /^[\da-zA-Z_-]+$/, + NAME_REGEX, "Can only contain lower/uppercase letters, underscore and minus.", ), ); @@ -299,7 +299,7 @@ export type ResultFiltersGroupItem = export const TagNameSchema = z .string() - .regex(/^[0-9a-zA-Z_.-]+$/) + .regex(NAME_REGEX) .max(16); export type TagName = z.infer; diff --git a/packages/schemas/src/util.ts b/packages/schemas/src/util.ts index 6098b9ab3a7b..f65f24852f94 100644 --- a/packages/schemas/src/util.ts +++ b/packages/schemas/src/util.ts @@ -9,6 +9,9 @@ export const StringNumberSchema = z .or(z.number().transform(String)); export type StringNumber = z.infer; +export const NAME_REGEX = /^[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+)*$/; +export const SLUG_REGEX = /^[0-9a-zA-Z_.-]+$/; + export const token = (): ZodString => z.string().regex(/^[a-zA-Z0-9_]+$/); export const IdSchema = token(); From 4b170f00a2997bc814a683e4ec5d7064554fb988 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Mon, 23 Mar 2026 21:42:37 +0200 Subject: [PATCH 2/8] refactor --- packages/schemas/src/ape-keys.ts | 7 ++--- packages/schemas/src/presets.ts | 7 ++--- packages/schemas/src/users.ts | 54 +++++++++++--------------------- packages/schemas/src/util.ts | 22 ++++++++++--- 4 files changed, 40 insertions(+), 50 deletions(-) diff --git a/packages/schemas/src/ape-keys.ts b/packages/schemas/src/ape-keys.ts index 21a29aa7bb6b..1022ea028c73 100644 --- a/packages/schemas/src/ape-keys.ts +++ b/packages/schemas/src/ape-keys.ts @@ -1,10 +1,7 @@ import { z } from "zod"; -import { IdSchema, SLUG_REGEX } from "./util"; +import { IdSchema, slug } from "./util"; -export const ApeKeyNameSchema = z - .string() - .regex(SLUG_REGEX) - .max(20); +export const ApeKeyNameSchema = slug().max(20); export const ApeKeyUserDefinedSchema = z.object({ name: ApeKeyNameSchema, diff --git a/packages/schemas/src/presets.ts b/packages/schemas/src/presets.ts index fc8dd5414bd7..f92dcc0b2162 100644 --- a/packages/schemas/src/presets.ts +++ b/packages/schemas/src/presets.ts @@ -1,15 +1,12 @@ import { z } from "zod"; -import { IdSchema, NAME_REGEX, TagSchema } from "./util"; +import { IdSchema, nameWithUnderscores, TagSchema } from "./util"; import { ConfigGroupName, ConfigGroupNameSchema, PartialConfigSchema, } from "./configs"; -export const PresetNameSchema = z - .string() - .regex(NAME_REGEX) - .max(16); +export const PresetNameSchema = nameWithUnderscores().max(16); export type PresetName = z.infer; export const PresetTypeSchema = z.enum(["full", "partial"]); diff --git a/packages/schemas/src/users.ts b/packages/schemas/src/users.ts index 66238489baa1..3a6fa2ad7ba3 100644 --- a/packages/schemas/src/users.ts +++ b/packages/schemas/src/users.ts @@ -1,5 +1,10 @@ import { z, ZodEffects, ZodOptional, ZodString } from "zod"; -import { IdSchema, NAME_REGEX, SLUG_REGEX, StringNumberSchema } from "./util"; +import { + IdSchema, + nameWithUnderscores, + slug, + StringNumberSchema, +} from "./util"; import { LanguageSchema } from "./languages"; import { ModeSchema, @@ -18,10 +23,7 @@ import { ConnectionSchema } from "./connections"; const NoneFilterSchema = z.literal("none"); export const ResultFiltersSchema = z.object({ _id: IdSchema, - name: z - .string() - .regex(NAME_REGEX) - .max(16), + name: nameWithUnderscores().max(16), pb: z .object({ no: z.boolean(), @@ -72,11 +74,13 @@ export const UserStreakSchema = z }) .strict(); export type UserStreak = z.infer; +export const TagNameSchema = nameWithUnderscores().max(16); +export type TagName = z.infer; export const UserTagSchema = z .object({ _id: IdSchema, - name: z.string(), + name: TagNameSchema, personalBests: PersonalBestsSchema, }) .strict(); @@ -90,19 +94,13 @@ function profileDetailsBase( .transform((value) => (value === null ? undefined : value)); } -export const TwitterProfileSchema = profileDetailsBase( - z - .string() - .max(20) - .regex(SLUG_REGEX), -).or(z.literal("")); +export const TwitterProfileSchema = profileDetailsBase(slug().max(20)).or( + z.literal(""), +); -export const GithubProfileSchema = profileDetailsBase( - z - .string() - .max(39) - .regex(SLUG_REGEX), -).or(z.literal("")); +export const GithubProfileSchema = profileDetailsBase(slug().max(39)).or( + z.literal(""), +); export const WebsiteSchema = profileDetailsBase( z.string().url().max(200).startsWith("https://"), @@ -125,10 +123,7 @@ export const UserProfileDetailsSchema = z .strict(); export type UserProfileDetails = z.infer; -export const CustomThemeNameSchema = z - .string() - .regex(NAME_REGEX) - .max(16); +export const CustomThemeNameSchema = nameWithUnderscores().max(16); export type CustomThemeName = z.infer; export const CustomThemeSchema = z @@ -244,14 +239,7 @@ export type FavoriteQuotes = z.infer; export const UserEmailSchema = z.string().email(); export const UserNameSchema = doesNotContainProfanity( "substring", - z - .string() - .min(1) - .max(16) - .regex( - NAME_REGEX, - "Can only contain lower/uppercase letters, underscore and minus.", - ), + nameWithUnderscores().min(1).max(16), ); export const UserSchema = z.object({ @@ -297,12 +285,6 @@ export type ResultFiltersGroup = keyof ResultFilters; export type ResultFiltersGroupItem = keyof ResultFilters[T]; -export const TagNameSchema = z - .string() - .regex(NAME_REGEX) - .max(16); -export type TagName = z.infer; - export const TypingStatsSchema = z.object({ completedTests: z.number().int().nonnegative().optional(), startedTests: z.number().int().nonnegative().optional(), diff --git a/packages/schemas/src/util.ts b/packages/schemas/src/util.ts index f65f24852f94..848280886deb 100644 --- a/packages/schemas/src/util.ts +++ b/packages/schemas/src/util.ts @@ -9,15 +9,29 @@ export const StringNumberSchema = z .or(z.number().transform(String)); export type StringNumber = z.infer; -export const NAME_REGEX = /^[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+)*$/; -export const SLUG_REGEX = /^[0-9a-zA-Z_.-]+$/; - export const token = (): ZodString => z.string().regex(/^[a-zA-Z0-9_]+$/); +export const slug = (): ZodString => + z + .string() + .regex( + /^[0-9a-zA-Z_.-]+$/, + "Only letters, numbers, underscores, dots and hyphens allowed", + ); + +export const nameWithUnderscores = (): ZodString => + z + .string() + .regex(/^[0-9a-zA-Z_]+$/, "Only letters, numbers, and underscores allowed") + .regex( + /^[a-zA-Z0-9]+(?:_[a-zA-Z0-9]+)*$/, + "Underscores cannot be at the start or end, or appear multiple times in a row", + ); + export const IdSchema = token(); export type Id = z.infer; -export const TagSchema = token().max(50); +export const TagSchema = nameWithUnderscores().max(50); export type Tag = z.infer; export const NullableStringSchema = z From 4607b994cb5ef6b52af54fc5ddf0758dab588926 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Mon, 23 Mar 2026 21:49:09 +0200 Subject: [PATCH 3/8] add test --- packages/schemas/__tests__/util.spec.ts | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 packages/schemas/__tests__/util.spec.ts diff --git a/packages/schemas/__tests__/util.spec.ts b/packages/schemas/__tests__/util.spec.ts new file mode 100644 index 000000000000..8e984a1a0681 --- /dev/null +++ b/packages/schemas/__tests__/util.spec.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from "vitest"; +import { nameWithUnderscores, slug } from "../src/util"; + +describe("Schema Validation Tests", () => { + describe("nameWithUnderscores", () => { + const schema = nameWithUnderscores(); + + it("accepts valid names", () => { + expect(schema.safeParse("valid_name").success).toBe(true); + expect(schema.safeParse("valid123").success).toBe(true); + expect(schema.safeParse("Valid_Name_Check").success).toBe(true); + }); + + it("rejects leading/trailing underscores", () => { + expect(schema.safeParse("_invalid").success).toBe(false); + expect(schema.safeParse("invalid_").success).toBe(false); + }); + + it("rejects consecutive underscores", () => { + expect(schema.safeParse("inv__alid").success).toBe(false); + }); + + it("rejects non-underscore separators", () => { + expect(schema.safeParse("invalid-name").success).toBe(false); + }); + }); + + describe("slug", () => { + const schema = slug(); + + it("accepts valid slugs", () => { + expect(schema.safeParse("valid-slug.123_test").success).toBe(true); + expect(schema.safeParse("valid.dots").success).toBe(true); + expect(schema.safeParse("_leading_is_fine_in_slug").success).toBe(true); + expect(schema.safeParse("trailing_is_fine_in_slug_").success).toBe(true); + }); + }); +}); From 1f8ee4c84b121f6e419577f0be13cb781f90a0cc Mon Sep 17 00:00:00 2001 From: byseif21 Date: Mon, 23 Mar 2026 21:58:12 +0200 Subject: [PATCH 4/8] fix --- packages/schemas/src/util.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/schemas/src/util.ts b/packages/schemas/src/util.ts index 848280886deb..3c459c424440 100644 --- a/packages/schemas/src/util.ts +++ b/packages/schemas/src/util.ts @@ -8,14 +8,14 @@ export const StringNumberSchema = z ) .or(z.number().transform(String)); export type StringNumber = z.infer; - export const token = (): ZodString => z.string().regex(/^[a-zA-Z0-9_]+$/); +export const SLUG_REGEX = /^[0-9a-zA-Z_.-]+$/; export const slug = (): ZodString => z .string() .regex( - /^[0-9a-zA-Z_.-]+$/, + SLUG_REGEX, "Only letters, numbers, underscores, dots and hyphens allowed", ); From b9ecf28adab8ff705931cdf2a92c09a53a9bd566 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Mon, 23 Mar 2026 22:10:15 +0200 Subject: [PATCH 5/8] reivew fix --- packages/schemas/__tests__/util.spec.ts | 21 ++++++++++----------- packages/schemas/src/presets.ts | 4 ++-- packages/schemas/src/users.ts | 15 +++++---------- packages/schemas/src/util.ts | 13 ++++++++----- 4 files changed, 25 insertions(+), 28 deletions(-) diff --git a/packages/schemas/__tests__/util.spec.ts b/packages/schemas/__tests__/util.spec.ts index 8e984a1a0681..e47a81507d78 100644 --- a/packages/schemas/__tests__/util.spec.ts +++ b/packages/schemas/__tests__/util.spec.ts @@ -1,27 +1,26 @@ import { describe, it, expect } from "vitest"; -import { nameWithUnderscores, slug } from "../src/util"; +import { nameWithSeparators, slug } from "../src/util"; describe("Schema Validation Tests", () => { - describe("nameWithUnderscores", () => { - const schema = nameWithUnderscores(); + describe("nameWithSeparators", () => { + const schema = nameWithSeparators(); it("accepts valid names", () => { expect(schema.safeParse("valid_name").success).toBe(true); + expect(schema.safeParse("valid-name").success).toBe(true); expect(schema.safeParse("valid123").success).toBe(true); - expect(schema.safeParse("Valid_Name_Check").success).toBe(true); + expect(schema.safeParse("Valid_Name-Check").success).toBe(true); }); - it("rejects leading/trailing underscores", () => { + it("rejects leading/trailing separators", () => { expect(schema.safeParse("_invalid").success).toBe(false); - expect(schema.safeParse("invalid_").success).toBe(false); + expect(schema.safeParse("invalid-").success).toBe(false); }); - it("rejects consecutive underscores", () => { + it("rejects consecutive separators", () => { expect(schema.safeParse("inv__alid").success).toBe(false); - }); - - it("rejects non-underscore separators", () => { - expect(schema.safeParse("invalid-name").success).toBe(false); + expect(schema.safeParse("inv--alid").success).toBe(false); + expect(schema.safeParse("inv-_alid").success).toBe(false); }); }); diff --git a/packages/schemas/src/presets.ts b/packages/schemas/src/presets.ts index f92dcc0b2162..91de2dad4b14 100644 --- a/packages/schemas/src/presets.ts +++ b/packages/schemas/src/presets.ts @@ -1,12 +1,12 @@ import { z } from "zod"; -import { IdSchema, nameWithUnderscores, TagSchema } from "./util"; +import { IdSchema, nameWithSeparators, TagSchema } from "./util"; import { ConfigGroupName, ConfigGroupNameSchema, PartialConfigSchema, } from "./configs"; -export const PresetNameSchema = nameWithUnderscores().max(16); +export const PresetNameSchema = nameWithSeparators().max(16); export type PresetName = z.infer; export const PresetTypeSchema = z.enum(["full", "partial"]); diff --git a/packages/schemas/src/users.ts b/packages/schemas/src/users.ts index 3a6fa2ad7ba3..9f734264ab19 100644 --- a/packages/schemas/src/users.ts +++ b/packages/schemas/src/users.ts @@ -1,10 +1,5 @@ import { z, ZodEffects, ZodOptional, ZodString } from "zod"; -import { - IdSchema, - nameWithUnderscores, - slug, - StringNumberSchema, -} from "./util"; +import { IdSchema, nameWithSeparators, slug, StringNumberSchema } from "./util"; import { LanguageSchema } from "./languages"; import { ModeSchema, @@ -23,7 +18,7 @@ import { ConnectionSchema } from "./connections"; const NoneFilterSchema = z.literal("none"); export const ResultFiltersSchema = z.object({ _id: IdSchema, - name: nameWithUnderscores().max(16), + name: nameWithSeparators().max(16), pb: z .object({ no: z.boolean(), @@ -74,7 +69,7 @@ export const UserStreakSchema = z }) .strict(); export type UserStreak = z.infer; -export const TagNameSchema = nameWithUnderscores().max(16); +export const TagNameSchema = nameWithSeparators().max(16); export type TagName = z.infer; export const UserTagSchema = z @@ -123,7 +118,7 @@ export const UserProfileDetailsSchema = z .strict(); export type UserProfileDetails = z.infer; -export const CustomThemeNameSchema = nameWithUnderscores().max(16); +export const CustomThemeNameSchema = nameWithSeparators().max(16); export type CustomThemeName = z.infer; export const CustomThemeSchema = z @@ -239,7 +234,7 @@ export type FavoriteQuotes = z.infer; export const UserEmailSchema = z.string().email(); export const UserNameSchema = doesNotContainProfanity( "substring", - nameWithUnderscores().min(1).max(16), + nameWithSeparators().min(1).max(16), ); export const UserSchema = z.object({ diff --git a/packages/schemas/src/util.ts b/packages/schemas/src/util.ts index 3c459c424440..ae4a6be61c68 100644 --- a/packages/schemas/src/util.ts +++ b/packages/schemas/src/util.ts @@ -19,19 +19,22 @@ export const slug = (): ZodString => "Only letters, numbers, underscores, dots and hyphens allowed", ); -export const nameWithUnderscores = (): ZodString => +export const nameWithSeparators = (): ZodString => z .string() - .regex(/^[0-9a-zA-Z_]+$/, "Only letters, numbers, and underscores allowed") .regex( - /^[a-zA-Z0-9]+(?:_[a-zA-Z0-9]+)*$/, - "Underscores cannot be at the start or end, or appear multiple times in a row", + /^[0-9a-zA-Z_-]+$/, + "Only letters, numbers, underscores and hyphens allowed", + ) + .regex( + /^[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+)*$/, + "Separators cannot be at the start or end, or appear multiple times in a row", ); export const IdSchema = token(); export type Id = z.infer; -export const TagSchema = nameWithUnderscores().max(50); +export const TagSchema = token().max(50); export type Tag = z.infer; export const NullableStringSchema = z From 7776172c2a5fd116e40b73f0302f4893552339f6 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Wed, 25 Mar 2026 12:27:05 +0200 Subject: [PATCH 6/8] prevent starting with . and remove mismatch --- frontend/src/ts/utils/misc.ts | 9 ++------- packages/schemas/src/users.ts | 2 +- packages/schemas/src/util.ts | 6 +++--- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index 0c2b4e5ac0ce..c8e9626bea23 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -2,8 +2,7 @@ import { lastElementFromArray } from "./arrays"; import { Config } from "@monkeytype/schemas/configs"; import { Mode, Mode2, PersonalBests } from "@monkeytype/schemas/shared"; import { Result } from "@monkeytype/schemas/results"; -import { RankAndCount } from "@monkeytype/schemas/users"; -import { SLUG_REGEX } from "@monkeytype/schemas/util"; +import { RankAndCount, UserNameSchema } from "@monkeytype/schemas/users"; import { roundTo2 } from "@monkeytype/util/numbers"; import { animate, AnimationParams } from "animejs"; import { ElementWithUtils } from "./dom"; @@ -148,11 +147,7 @@ export function escapeHTML(str: T): T { export function isUsernameValid(name: string): boolean { if (name === null || name === undefined || name === "") return false; - if (name.toLowerCase().includes("miodec")) return false; - if (name.toLowerCase().includes("bitly")) return false; - if (name.length > 14) return false; - if (/^\..*/.test(name.toLowerCase())) return false; - return SLUG_REGEX.test(name); + return UserNameSchema.safeParse(name).success; } export function clearTimeouts(timeouts: (number | NodeJS.Timeout)[]): void { diff --git a/packages/schemas/src/users.ts b/packages/schemas/src/users.ts index 9f734264ab19..204309791662 100644 --- a/packages/schemas/src/users.ts +++ b/packages/schemas/src/users.ts @@ -234,7 +234,7 @@ export type FavoriteQuotes = z.infer; export const UserEmailSchema = z.string().email(); export const UserNameSchema = doesNotContainProfanity( "substring", - nameWithSeparators().min(1).max(16), + slug().min(1).max(16), ); export const UserSchema = z.object({ diff --git a/packages/schemas/src/util.ts b/packages/schemas/src/util.ts index ae4a6be61c68..4abbf2c39c47 100644 --- a/packages/schemas/src/util.ts +++ b/packages/schemas/src/util.ts @@ -10,14 +10,14 @@ export const StringNumberSchema = z export type StringNumber = z.infer; export const token = (): ZodString => z.string().regex(/^[a-zA-Z0-9_]+$/); -export const SLUG_REGEX = /^[0-9a-zA-Z_.-]+$/; export const slug = (): ZodString => z .string() .regex( - SLUG_REGEX, + /^[0-9a-zA-Z_.-]+$/, "Only letters, numbers, underscores, dots and hyphens allowed", - ); + ) + .regex(/^[^.].*$/, "Cannot start with a dot"); export const nameWithSeparators = (): ZodString => z From 05cc5cce4427414e84a269e3d250494128ef9e28 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Wed, 25 Mar 2026 13:00:14 +0200 Subject: [PATCH 7/8] use slug with filters safely --- packages/schemas/src/users.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/schemas/src/users.ts b/packages/schemas/src/users.ts index 204309791662..a2a7704e39bf 100644 --- a/packages/schemas/src/users.ts +++ b/packages/schemas/src/users.ts @@ -18,7 +18,7 @@ import { ConnectionSchema } from "./connections"; const NoneFilterSchema = z.literal("none"); export const ResultFiltersSchema = z.object({ _id: IdSchema, - name: nameWithSeparators().max(16), + name: slug().max(16), pb: z .object({ no: z.boolean(), From 98bb7033ef638453ad6aebfe01d6a7d50d6adc75 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Wed, 25 Mar 2026 13:03:12 +0200 Subject: [PATCH 8/8] update tests --- packages/schemas/__tests__/util.spec.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/schemas/__tests__/util.spec.ts b/packages/schemas/__tests__/util.spec.ts index e47a81507d78..8f29eb0496b9 100644 --- a/packages/schemas/__tests__/util.spec.ts +++ b/packages/schemas/__tests__/util.spec.ts @@ -22,6 +22,11 @@ describe("Schema Validation Tests", () => { expect(schema.safeParse("inv--alid").success).toBe(false); expect(schema.safeParse("inv-_alid").success).toBe(false); }); + + it("rejects dots", () => { + expect(schema.safeParse("invalid.dot").success).toBe(false); + expect(schema.safeParse(".invalid").success).toBe(false); + }); }); describe("slug", () => { @@ -30,8 +35,22 @@ describe("Schema Validation Tests", () => { it("accepts valid slugs", () => { expect(schema.safeParse("valid-slug.123_test").success).toBe(true); expect(schema.safeParse("valid.dots").success).toBe(true); - expect(schema.safeParse("_leading_is_fine_in_slug").success).toBe(true); + expect(schema.safeParse("_leading_underscore_is_fine").success).toBe( + true, + ); + expect(schema.safeParse("-leading_hyphen_is_fine").success).toBe(true); expect(schema.safeParse("trailing_is_fine_in_slug_").success).toBe(true); }); + + it("rejects leading dots", () => { + expect(schema.safeParse(".invalid").success).toBe(false); + }); + + it("rejects invalid characters", () => { + expect(schema.safeParse("invalid,comma").success).toBe(false); + expect(schema.safeParse(",invalid").success).toBe(false); + expect(schema.safeParse("invalid space").success).toBe(false); + expect(schema.safeParse("invalid#hash").success).toBe(false); + }); }); });