http-url.ts59 lines · main
1import { z } from 'zod'
2
3const HTTP_URL_PROTOCOL_REGEX = /^https?:\/\//
4const IPV4_SEGMENT = '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)'
5const IPV4_REGEX = new RegExp(`^(?:${IPV4_SEGMENT}\\.){3}${IPV4_SEGMENT}$`)
6const BRACKETED_IPV6_REGEX = /^\[[0-9a-f:.]+\]$/i
7
8export const hasHttpUrlProtocol = (value: string) => HTTP_URL_PROTOCOL_REGEX.test(value)
9
10export const isValidHttpEndpointUrl = (value: string) => {
11 try {
12 const url = new URL(value)
13 if (url.protocol !== 'http:' && url.protocol !== 'https:') return false
14
15 const { hostname } = url
16 return (
17 hostname === 'localhost' ||
18 hostname.includes('.') ||
19 IPV4_REGEX.test(hostname) ||
20 BRACKETED_IPV6_REGEX.test(hostname)
21 )
22 } catch {
23 return false
24 }
25}
26
27type HttpEndpointUrlSchemaOptions = {
28 requiredMessage: string
29 invalidMessage: string
30 prefixMessage: string
31}
32
33export const httpEndpointUrlSchema = ({
34 requiredMessage,
35 invalidMessage,
36 prefixMessage,
37}: HttpEndpointUrlSchemaOptions) =>
38 z
39 .string()
40 .trim()
41 .min(1, requiredMessage)
42 .superRefine((value, ctx) => {
43 if (!value) return
44
45 if (!hasHttpUrlProtocol(value)) {
46 ctx.addIssue({
47 code: z.ZodIssueCode.custom,
48 message: prefixMessage,
49 })
50 return
51 }
52
53 if (!isValidHttpEndpointUrl(value)) {
54 ctx.addIssue({
55 code: z.ZodIssueCode.custom,
56 message: invalidMessage,
57 })
58 }
59 })