Skip to main content
Glama
phone-validation.ts5.59 kB
import type { CountryCode, ParseError, PhoneNumber } from 'libphonenumber-js'; import * as libFull from 'libphonenumber-js'; import * as libMin from 'libphonenumber-js/min'; export const PHONE_METADATA_SOURCE = process.env.ATTIO_PHONE_METADATA === 'min' ? 'min' : 'default'; const { parsePhoneNumberFromString, isValidPhoneNumber: libIsValidPhoneNumber, isPossiblePhoneNumber: libIsPossiblePhoneNumber, validatePhoneNumberLength, } = PHONE_METADATA_SOURCE === 'min' ? libMin : libFull; const DEFAULT_COUNTRY = (process.env.DEFAULT_PHONE_COUNTRY || 'US') as CountryCode; type PhoneValidationErrorCode = | 'INVALID_FORMAT' | 'INVALID_COUNTRY_CODE' | 'TOO_SHORT' | 'TOO_LONG' | 'NOT_A_NUMBER' | 'INVALID_TYPE' | 'UNKNOWN_ERROR'; export class PhoneValidationError extends Error { readonly code: PhoneValidationErrorCode; readonly input: string; readonly country?: CountryCode; readonly cause?: ParseError | Error; constructor( code: PhoneValidationErrorCode, message: string, input: string, country?: CountryCode, cause?: ParseError | Error ) { super(message); this.name = 'PhoneValidationError'; this.code = code; this.input = input; this.country = country; this.cause = cause; } } export interface PhoneValidationResult { valid: boolean; possible: boolean; e164?: string; national?: string; country?: CountryCode; error?: PhoneValidationError; } function resolveCountry(country?: string): CountryCode { if (!country) { return DEFAULT_COUNTRY; } return country.toUpperCase() as CountryCode; } function mapLengthError( issue: ReturnType<typeof validatePhoneNumberLength>, input: string, country?: CountryCode ): PhoneValidationError | undefined { switch (issue) { case 'INVALID_COUNTRY': return new PhoneValidationError( 'INVALID_COUNTRY_CODE', 'Invalid or unsupported country calling code provided for phone number.', input, country ); case 'TOO_SHORT': return new PhoneValidationError( 'TOO_SHORT', 'Phone number is too short to be valid.', input, country ); case 'TOO_LONG': return new PhoneValidationError( 'TOO_LONG', 'Phone number is too long to be valid.', input, country ); case 'NOT_A_NUMBER': return new PhoneValidationError( 'NOT_A_NUMBER', 'Input does not appear to be a valid phone number.', input, country ); default: return undefined; } } export function isPossiblePhoneNumber( value: string, country?: string ): boolean { const candidate = value?.trim(); if (!candidate) return false; const resolvedCountry = resolveCountry(country); try { return libIsPossiblePhoneNumber(candidate, resolvedCountry); } catch { return false; } } export function isValidPhoneNumber(value: string, country?: string): boolean { const candidate = value?.trim(); if (!candidate) return false; const resolvedCountry = resolveCountry(country); try { return libIsValidPhoneNumber(candidate, resolvedCountry); } catch { return false; } } function safeParse( value: string, country: CountryCode ): PhoneNumber | undefined { try { return parsePhoneNumberFromString(value, country) || undefined; } catch (error) { return undefined; } } export function validatePhoneNumber( value: string, country?: string ): PhoneValidationResult { if (typeof value !== 'string') { return { valid: false, possible: false, error: new PhoneValidationError( 'INVALID_TYPE', 'Phone numbers must be provided as strings.', String(value) ), }; } const trimmed = value.trim(); if (!trimmed) { return { valid: false, possible: false, error: new PhoneValidationError( 'INVALID_FORMAT', 'Phone number cannot be empty.', value ), }; } const resolvedCountry = resolveCountry(country); const possible = isPossiblePhoneNumber(trimmed, resolvedCountry); const valid = isValidPhoneNumber(trimmed, resolvedCountry); let error: PhoneValidationError | undefined; if (!possible) { const lengthIssue = validatePhoneNumberLength(trimmed, resolvedCountry); error = mapLengthError(lengthIssue, value, resolvedCountry) || new PhoneValidationError( 'INVALID_FORMAT', 'Phone number format is not recognized.', value, resolvedCountry ); } else if (!valid) { error = new PhoneValidationError( 'INVALID_FORMAT', 'Phone number format is possible but not valid for the specified region.', value, resolvedCountry ); } const parsed = safeParse(trimmed, resolvedCountry); if (parsed?.isValid()) { return { valid: true, possible, country: parsed.country, national: parsed.formatNational(), e164: parsed.number, }; } return { valid, possible, error, }; } export function toE164OrNull( value: unknown, defaultCountry?: string ): string | null { if (typeof value !== 'string') { return null; } const sanitized = value.trim(); if (!sanitized) return null; const resolvedCountry = resolveCountry(defaultCountry); const parsed = safeParse(sanitized, resolvedCountry); return parsed && parsed.isValid() ? parsed.number : null; } export function toE164(value: unknown, defaultCountry?: string): string | null { return toE164OrNull(value, defaultCountry); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kesslerio/attio-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server