@phonecheck/phone-number-validator-js
Version:
Validate, parse, and enrich international phone numbers — geocoding, carrier lookup, and timezone resolution. Sync (Node) + async (serverless) APIs, platform adapters, and a CLI.
362 lines (353 loc) • 12 kB
JavaScript
;
var bson = require('bson');
var tinyLru = require('tiny-lru');
var node_fs = require('node:fs');
var node_path = require('node:path');
var node_url = require('node:url');
var libphonenumberJs = require('libphonenumber-js');
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
const DEFAULT_CACHE_SIZE = 100;
let cache = tinyLru.lru(DEFAULT_CACHE_SIZE);
let maxSize = DEFAULT_CACHE_SIZE;
function cacheGet(key) {
return cache.get(key);
}
function cacheSet(key, value) {
cache.set(key, value);
}
function clearCache() {
cache.clear();
}
function getCacheSize() {
return cache.size;
}
function getCacheStats() {
return { size: cache.size, maxSize };
}
function setCacheSize(size) {
if (size <= 0) {
throw new Error(`Cache size must be > 0 (got ${size})`);
}
if (size === maxSize)
return;
const previous = cache;
cache = tinyLru.lru(size);
maxSize = size;
const entries = [...previous.entries()].reverse();
for (const [key, value] of entries) {
if (cache.size >= size)
break;
cache.set(key, value);
}
}
const DEFAULT_LOCALE = "en";
const TIMEZONES_PATH = "timezones.bson";
let activeLoader = null;
function setResourceLoader(loader) {
activeLoader = loader;
}
function getResourceLoader() {
return activeLoader;
}
function findByPrefix(table, key) {
for (let prefix = key; prefix.length > 0; prefix = prefix.slice(0, -1)) {
const value = table[prefix];
if (typeof value === "string" && value.length > 0)
return value;
}
return null;
}
function decodeBson(bytes) {
if (!bytes || bytes.byteLength === 0)
return null;
const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
return bson.deserialize(buf);
}
function logLoadError(path, err) {
if (process.env.NODE_ENV !== "production") {
console.error(`Error loading data from ${path}:`, err);
}
}
function rememberTable(path, bytes) {
const table = decodeBson(bytes);
if (!table)
return null;
cacheSet(path, table);
return table;
}
async function loadTableAsync(path) {
if (!activeLoader)
return null;
const cached = cacheGet(path);
if (cached)
return cached;
try {
return rememberTable(path, await activeLoader.loadResource(path));
} catch (err) {
logLoadError(path, err);
return null;
}
}
function loadTableSync(path) {
if (!(activeLoader === null || activeLoader === void 0 ? void 0 : activeLoader.loadResourceSync))
return null;
const cached = cacheGet(path);
if (cached)
return cached;
try {
return rememberTable(path, activeLoader.loadResourceSync(path));
} catch (err) {
logLoadError(path, err);
return null;
}
}
function localeResourcePath(kind, locale, countryCode) {
return `${kind}/${locale}/${countryCode}.bson`;
}
function localePaths(kind, locale, cc) {
if (locale === DEFAULT_LOCALE)
return [localeResourcePath(kind, DEFAULT_LOCALE, cc)];
return [localeResourcePath(kind, locale, cc), localeResourcePath(kind, DEFAULT_LOCALE, cc)];
}
function partsOf(phoneNumber) {
var _a, _b;
const national = (_a = phoneNumber === null || phoneNumber === void 0 ? void 0 : phoneNumber.nationalNumber) === null || _a === void 0 ? void 0 : _a.toString();
const cc = (_b = phoneNumber === null || phoneNumber === void 0 ? void 0 : phoneNumber.countryCallingCode) === null || _b === void 0 ? void 0 : _b.toString();
if (!national || !cc)
return null;
return { national, cc };
}
function timezoneKey(phoneNumber) {
var _a;
const raw = (_a = phoneNumber === null || phoneNumber === void 0 ? void 0 : phoneNumber.number) === null || _a === void 0 ? void 0 : _a.toString();
if (!raw)
return null;
return raw.startsWith("+") ? raw.slice(1) : raw;
}
function parseTimezones(value) {
if (!value)
return null;
const zones = value.split("&").filter((z) => z.length > 0);
return zones.length > 0 ? zones : null;
}
async function localizedAsync(kind, phoneNumber, locale) {
const parts = partsOf(phoneNumber);
if (!parts)
return null;
for (const path of localePaths(kind, locale, parts.cc)) {
const table = await loadTableAsync(path);
const hit = table ? findByPrefix(table, parts.national) : null;
if (hit)
return hit;
}
return null;
}
function localizedSync(kind, phoneNumber, locale) {
const parts = partsOf(phoneNumber);
if (!parts)
return null;
for (const path of localePaths(kind, locale, parts.cc)) {
const table = loadTableSync(path);
const hit = table ? findByPrefix(table, parts.national) : null;
if (hit)
return hit;
}
return null;
}
function geocoder(phoneNumber, locale = DEFAULT_LOCALE) {
return localizedSync("geocodes", phoneNumber, locale);
}
function carrier(phoneNumber, locale = DEFAULT_LOCALE) {
return localizedSync("carrier", phoneNumber, locale);
}
function timezones(phoneNumber) {
const key = timezoneKey(phoneNumber);
if (!key)
return null;
const table = loadTableSync(TIMEZONES_PATH);
return table ? parseTimezones(findByPrefix(table, key)) : null;
}
async function geocoderAsync(phoneNumber, locale = DEFAULT_LOCALE) {
return localizedAsync("geocodes", phoneNumber, locale);
}
async function carrierAsync(phoneNumber, locale = DEFAULT_LOCALE) {
return localizedAsync("carrier", phoneNumber, locale);
}
async function timezonesAsync(phoneNumber) {
const key = timezoneKey(phoneNumber);
if (!key)
return null;
const table = await loadTableAsync(TIMEZONES_PATH);
return table ? parseTimezones(findByPrefix(table, key)) : null;
}
async function enrichPhoneNumber(phoneNumber, options = {}) {
const [geocode, car, tz] = await Promise.all([
geocoderAsync(phoneNumber, options.locale),
carrierAsync(phoneNumber, options.carrierLocale),
timezonesAsync(phoneNumber)
]);
return { geocode, carrier: car, timezones: tz };
}
function defaultResourcesDir() {
let here;
try {
here = __dirname;
} catch {
here = node_path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.js', document.baseURI).href))));
}
let dir = here;
for (let i = 0; i < 6; i++) {
const candidate = node_path.resolve(dir, "resources");
if (node_fs.existsSync(node_path.resolve(candidate, "timezones.bson"))) {
return candidate;
}
const parent = node_path.resolve(dir, "..");
if (parent === dir)
break;
dir = parent;
}
return node_path.resolve(here, "..", "resources");
}
class NodeFsResourceLoader {
constructor(options = {}) {
var _a;
this.baseDir = (_a = options.resourcesDir) !== null && _a !== void 0 ? _a : defaultResourcesDir();
}
async loadResource(path) {
return this.loadResourceSync(path);
}
loadResourceSync(path) {
const fullPath = node_path.join(this.baseDir, path);
if (!node_fs.existsSync(fullPath))
return null;
return node_fs.readFileSync(fullPath);
}
}
function resolveOptions(options) {
var _a, _b, _c, _d, _e;
return {
defaultCountry: options.defaultCountry,
locale: (_a = options.locale) !== null && _a !== void 0 ? _a : "en",
carrierLocale: (_b = options.carrierLocale) !== null && _b !== void 0 ? _b : "en",
// Each enrichment toggle defaults to ON when unset — opt-out, not opt-in.
geocode: (_c = options.geocode) !== null && _c !== void 0 ? _c : true,
carrier: (_d = options.carrier) !== null && _d !== void 0 ? _d : true,
timezones: (_e = options.timezones) !== null && _e !== void 0 ? _e : true
};
}
async function validateOne(input, options) {
const parsed = libphonenumberJs.parsePhoneNumberFromString(input, options.defaultCountry);
if (!(parsed === null || parsed === void 0 ? void 0 : parsed.isValid())) {
return { input, valid: false, error: "Invalid or unparseable phone number" };
}
const [geocode, carrier, timezones] = await Promise.all([
options.geocode ? geocoderAsync(parsed, options.locale) : Promise.resolve(void 0),
options.carrier ? carrierAsync(parsed, options.carrierLocale) : Promise.resolve(void 0),
options.timezones ? timezonesAsync(parsed) : Promise.resolve(void 0)
]);
const result = {
input,
valid: true,
formatted: {
e164: parsed.format("E.164"),
international: parsed.formatInternational(),
national: parsed.formatNational(),
rfc3966: parsed.format("RFC3966")
},
country: parsed.country,
countryCallingCode: parsed.countryCallingCode.toString(),
nationalNumber: parsed.nationalNumber.toString(),
type: parsed.getType()
};
if (geocode !== void 0)
result.geocode = geocode;
if (carrier !== void 0)
result.carrier = carrier;
if (timezones !== void 0)
result.timezones = timezones;
return result;
}
async function executeValidation(dispatch) {
const opts = resolveOptions(dispatch.options);
if (dispatch.kind === "single") {
return validateOne(dispatch.phoneNumber, opts);
}
return Promise.all(dispatch.phoneNumbers.map((p) => validateOne(p, opts)));
}
async function validateSingle(input, options = {}) {
return validateOne(input, resolveOptions(options));
}
async function validateBatch(inputs, options = {}) {
const opts = resolveOptions(options);
return Promise.all(inputs.map((p) => validateOne(p, opts)));
}
const MAX_BATCH_SIZE = 100;
function extractBatchOptions(body) {
return {
defaultCountry: body.defaultCountry,
locale: body.locale,
carrierLocale: body.carrierLocale,
geocode: body.geocode,
carrier: body.carrier,
timezones: body.timezones
};
}
function validateBatchField(phoneNumbers) {
if (!Array.isArray(phoneNumbers) || phoneNumbers.length === 0) {
return { ok: false, status: 400, message: "phoneNumbers array is required" };
}
if (phoneNumbers.length > MAX_BATCH_SIZE) {
return { ok: false, status: 400, message: `Maximum ${MAX_BATCH_SIZE} phone numbers per batch` };
}
return { ok: true, phoneNumbers };
}
const MISSING_INPUT = {
kind: "invalid",
status: 400,
message: "phoneNumber or phoneNumbers array is required"
};
function classifyRequest(body) {
if (!body)
return MISSING_INPUT;
const options = extractBatchOptions(body);
if (body.phoneNumbers !== void 0) {
const validated = validateBatchField(body.phoneNumbers);
if (!validated.ok)
return { kind: "invalid", status: validated.status, message: validated.message };
return { kind: "batch", phoneNumbers: validated.phoneNumbers, options };
}
if (body.phoneNumber) {
return { kind: "single", phoneNumber: body.phoneNumber, options };
}
return MISSING_INPUT;
}
setResourceLoader(new NodeFsResourceLoader());
exports.DEFAULT_CACHE_SIZE = DEFAULT_CACHE_SIZE;
exports.MAX_BATCH_SIZE = MAX_BATCH_SIZE;
exports.NodeFsResourceLoader = NodeFsResourceLoader;
exports.carrier = carrier;
exports.carrierAsync = carrierAsync;
exports.classifyRequest = classifyRequest;
exports.clearCache = clearCache;
exports.enrichPhoneNumber = enrichPhoneNumber;
exports.executeValidation = executeValidation;
exports.extractBatchOptions = extractBatchOptions;
exports.geocoder = geocoder;
exports.geocoderAsync = geocoderAsync;
exports.getCacheSize = getCacheSize;
exports.getCacheStats = getCacheStats;
exports.getResourceLoader = getResourceLoader;
exports.setCacheSize = setCacheSize;
exports.setResourceLoader = setResourceLoader;
exports.timezones = timezones;
exports.timezonesAsync = timezonesAsync;
exports.validateBatch = validateBatch;
exports.validateBatchField = validateBatchField;
exports.validateSingle = validateSingle;
Object.keys(libphonenumberJs).forEach(function (k) {
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
enumerable: true,
get: function () { return libphonenumberJs[k]; }
});
});
//# sourceMappingURL=index.js.map