File
Blob: src/shared/slugs.ts
| 1 | // Shared slug policy used by namespace and repository slugs. Browser-safe so |
| 2 | // the same validator runs in SSR pages and Worker validation paths. |
| 3 | // |
| 4 | // The regex enforces lowercase ASCII alphanumerics with internal dashes. |
| 5 | // Length is bounded at 1..40. Reserved slugs cover Worker-owned routes and |
| 6 | // common static paths so that creating a namespace cannot shadow existing |
| 7 | // surfaces. |
| 8 | |
| 9 | const SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/; |
| 10 | export const SLUG_MAX_LENGTH = 40; |
| 11 | |
| 12 | export const RESERVED_SLUGS: readonly string[] = [ |
| 13 | "auth", |
| 14 | "sign-in", |
| 15 | "sign-out", |
| 16 | "api", |
| 17 | "assets", |
| 18 | "_cache", |
| 19 | "_routes", |
| 20 | "favicon.ico", |
| 21 | "robots.txt", |
| 22 | ]; |
| 23 | |
| 24 | const reservedSet: ReadonlySet<string> = new Set(RESERVED_SLUGS); |
| 25 | |
| 26 | export function isValidSlug(input: string): boolean { |
| 27 | if (typeof input !== "string") return false; |
| 28 | if (input.length === 0 || input.length > SLUG_MAX_LENGTH) return false; |
| 29 | if (!SLUG_PATTERN.test(input)) return false; |
| 30 | if (reservedSet.has(input)) return false; |
| 31 | return true; |
| 32 | } |
| 33 | |
| 34 | export type SlugValidation = |
| 35 | | { ok: true; slug: string } |
| 36 | | { ok: false; reason: "format" | "reserved" | "length" }; |
| 37 | |
| 38 | // Granular result so the route handler can decide which message to surface |
| 39 | // (taken vs format vs reserved) without re-checking the regex. |
| 40 | export function validateSlugForRoute(input: string): SlugValidation { |
| 41 | if (typeof input !== "string" || input.length === 0) { |
| 42 | return { ok: false, reason: "length" }; |
| 43 | } |
| 44 | if (input.length > SLUG_MAX_LENGTH) return { ok: false, reason: "length" }; |
| 45 | if (reservedSet.has(input)) return { ok: false, reason: "reserved" }; |
| 46 | if (!SLUG_PATTERN.test(input)) return { ok: false, reason: "format" }; |
| 47 | return { ok: true, slug: input }; |
| 48 | } |