http-url.ts 1.4 KB

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