├── .github ├── renovate.json └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .prettierrc.yaml ├── .scripts └── prepare.js ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── __tests__ ├── carrier.test.ts └── index.test.ts ├── jest.config.js ├── package.json ├── release.config.js ├── release.mjs ├── rollup.config.js ├── src ├── index.ts └── locales.ts ├── tsconfig.json └── yarn.lock /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "branchPrefix": "renovate-", 4 | "baseBranches": ["develop"], 5 | "assigneesFromCodeOwners": true, 6 | "packageRules": [ 7 | { 8 | "matchPackagePatterns": ["*"], 9 | "matchUpdateTypes": ["minor", "patch"], 10 | "groupName": "all non-major dependencies", 11 | "groupSlug": "all-minor-patch", 12 | "automerge": true, 13 | "labels": ["dependencies"] 14 | }, 15 | { 16 | "matchPackagePatterns": ["*"], 17 | "matchUpdateTypes": ["major"], 18 | "labels": ["dependencies", "breaking"] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | workflow_dispatch: 4 | workflow_call: 5 | push: 6 | paths: 7 | - 'src/**' 8 | - 'package.json' 9 | - 'yarn.lock' 10 | - 'release.config.js' 11 | - '.github/workflows/ci.yml' 12 | branches: 13 | - '*' 14 | - '**' 15 | - '!master' 16 | 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.ref }} 19 | cancel-in-progress: true 20 | 21 | 22 | env: 23 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | CI: true 26 | 27 | jobs: 28 | CI: 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 20 31 | 32 | permissions: 33 | packages: write 34 | contents: write 35 | 36 | steps: 37 | - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." 38 | 39 | - uses: actions/checkout@v4 40 | with: 41 | fetch-depth: 30 42 | persist-credentials: false 43 | 44 | - uses: FranzDiebold/github-env-vars-action@v2 45 | 46 | - name: Setup Node.js 47 | uses: actions/setup-node@v4 48 | with: 49 | node-version: lts/* 50 | 51 | - name: Yarn 52 | run: yarn install --frozen-lockfile 53 | 54 | - name: Test 55 | run: | 56 | yarn preparemetadata 57 | yarn test 58 | 59 | - name: Release 60 | if: github.ref == 'refs/heads/develop' 61 | run: | 62 | node release.mjs 63 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | workflow_dispatch: 4 | workflow_call: 5 | push: 6 | paths: 7 | - 'src/**' 8 | - 'yarn.lock' 9 | branches: 10 | - 'master' 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | env: 17 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | CI: true 20 | 21 | jobs: 22 | Release: 23 | runs-on: ubuntu-latest 24 | timeout-minutes: 20 25 | 26 | permissions: 27 | packages: write 28 | contents: write 29 | 30 | steps: 31 | - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." 32 | 33 | - uses: actions/checkout@v4 34 | with: 35 | fetch-depth: 30 36 | persist-credentials: false 37 | 38 | - uses: FranzDiebold/github-env-vars-action@v2 39 | 40 | - name: Setup Node.js 41 | uses: actions/setup-node@v4 42 | with: 43 | node-version: lts/* 44 | 45 | - name: Yarn 46 | run: yarn install --frozen-lockfile 47 | 48 | - name: Test 49 | run: | 50 | yarn preparemetadata 51 | yarn test 52 | 53 | - name: Release 54 | run: | 55 | node release.mjs 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | resources 2 | lib 3 | .DS_Store 4 | 5 | # Created by https://www.toptal.com/developers/gitignore/api/node 6 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 7 | 8 | ### Node ### 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | lerna-debug.log* 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | *.lcov 32 | 33 | # nyc test coverage 34 | .nyc_output 35 | 36 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | bower_components 41 | 42 | # node-waf configuration 43 | .lock-wscript 44 | 45 | # Compiled binary addons (https://nodejs.org/api/addons.html) 46 | build/Release 47 | 48 | # Dependency directories 49 | node_modules/ 50 | jspm_packages/ 51 | 52 | # TypeScript v1 declaration files 53 | typings/ 54 | 55 | # TypeScript cache 56 | *.tsbuildinfo 57 | 58 | # Optional npm cache directory 59 | .npm 60 | 61 | # Optional eslint cache 62 | .eslintcache 63 | 64 | # Microbundle cache 65 | .rpt2_cache/ 66 | .rts2_cache_cjs/ 67 | .rts2_cache_es/ 68 | .rts2_cache_umd/ 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variables file 80 | .env 81 | .env.test 82 | 83 | # parcel-bundler cache (https://parceljs.org/) 84 | .cache 85 | 86 | # Next.js build output 87 | .next 88 | 89 | # Nuxt.js build / generate output 90 | .nuxt 91 | dist 92 | 93 | # Gatsby files 94 | .cache/ 95 | # Comment in the public line in if your project uses Gatsby and not Next.js 96 | # https://nextjs.org/blog/next-9-1#public-directory-support 97 | # public 98 | 99 | # vuepress build output 100 | .vuepress/dist 101 | 102 | # Serverless directories 103 | .serverless/ 104 | 105 | # FuseBox cache 106 | .fusebox/ 107 | 108 | # DynamoDB Local files 109 | .dynamodb/ 110 | 111 | # TernJS port file 112 | .tern-port 113 | 114 | # Stores VSCode versions used for testing VSCode extensions 115 | .vscode-test 116 | 117 | # End of https://www.toptal.com/developers/gitignore/api/node 118 | 119 | .idea 120 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | trailingComma: "es5" 2 | tabWidth: 2 3 | semi: false 4 | singleQuote: true 5 | -------------------------------------------------------------------------------- /.scripts/prepare.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This script loads the geocoder and carrier data from the libphonenumber repository, 3 | * creates bson files from them and autogenerated the types in src/locales.ts 4 | */ 5 | const { 6 | readdirSync, 7 | writeFileSync, 8 | lstatSync, 9 | createReadStream, 10 | mkdirSync, 11 | } = require('fs') 12 | const { join, basename } = require('path') 13 | const { createInterface } = require('readline') 14 | const { execSync } = require('child_process') 15 | const BSON = require('bson') 16 | 17 | const isDir = (source) => lstatSync(source).isDirectory() 18 | 19 | async function prepareLocale(localePath, locale, type) { 20 | const fileRe = /[0-9]+\.txt/ 21 | const files = readdirSync(localePath).filter((source) => fileRe.test(source)) 22 | const lineRe = /^([0-9]+)\|(.*)$/ 23 | for (let i = 0; i < files.length; ++i) { 24 | let data = {} 25 | const file = files[i] 26 | const countryCode = basename(file, '.txt') 27 | const ccRe = new RegExp(`^${countryCode}`) 28 | const fileStream = createReadStream(join(localePath, file)) 29 | const rl = createInterface({ 30 | input: fileStream, 31 | crlfDelay: Infinity, 32 | }) 33 | // Note: we use the crlfDelay option to recognize all instances of CR LF 34 | // ('\r\n') in input.txt as a single line break. 35 | for await (const line of rl) { 36 | let m 37 | if ((m = lineRe.exec(line)) !== null) { 38 | const [_, nr, description] = m 39 | const prefix = nr.replace(ccRe, '') 40 | data[prefix] = description 41 | } 42 | } 43 | const bData = BSON.serialize(data) 44 | const dataPath = join(__dirname, '/../resources', type, locale) 45 | mkdirSync(dataPath, { recursive: true }) 46 | const filePath = join(dataPath, `${countryCode}.bson`) 47 | writeFileSync(filePath, bData) 48 | } 49 | } 50 | 51 | async function preparePath(dataPath, type) { 52 | const locales = readdirSync(dataPath).filter((source) => 53 | isDir(join(dataPath, source)) 54 | ) 55 | const promises = [] 56 | for (let i = 0; i < locales.length; ++i) { 57 | const locale = locales[i] 58 | // if (locale !== 'en') continue 59 | const localePath = join(dataPath, locale) 60 | promises.push(prepareLocale(localePath, locale, type)) 61 | } 62 | await Promise.all(promises) 63 | return { locales } 64 | } 65 | 66 | async function prepareTimezones() { 67 | const lineRe = /^([0-9]+)\|(.*)$/ 68 | let data = {} 69 | const file = join( 70 | __dirname, 71 | '/../resources/libphonenumber/resources/timezones/map_data.txt' 72 | ) 73 | const fileStream = createReadStream(file) 74 | const rl = createInterface({ 75 | input: fileStream, 76 | crlfDelay: Infinity, 77 | }) 78 | // Note: we use the crlfDelay option to recognize all instances of CR LF 79 | // ('\r\n') in input.txt as a single line break. 80 | for await (const line of rl) { 81 | let m 82 | if ((m = lineRe.exec(line)) !== null) { 83 | const [_, prefix, description] = m 84 | data[prefix] = description 85 | } 86 | } 87 | const bData = BSON.serialize(data) 88 | const filePath = join(__dirname, '/../resources/timezones.bson') 89 | writeFileSync(filePath, bData) 90 | } 91 | 92 | const prepare = async () => { 93 | mkdirSync(join(__dirname, '/../resources/geocodes'), { recursive: true }) 94 | mkdirSync(join(__dirname, '/../resources/carrier'), { recursive: true }) 95 | execSync( 96 | `cd ${join( 97 | __dirname, 98 | '/../resources' 99 | )} && git clone https://github.com/google/libphonenumber` 100 | ) 101 | console.log('Preparing metadata...') 102 | const dataBasePath = join(__dirname, '/../resources/libphonenumber/resources') 103 | let generatedTypes = '/* THIS FILE IS AUTOGENERATED. */\n' 104 | 105 | const geocodingPath = join(dataBasePath, 'geocoding') 106 | const { locales: geoLocales } = await preparePath(geocodingPath, 'geocodes') 107 | generatedTypes += `export type GeocoderLocale = ${geoLocales 108 | .map((l) => `'${l}'`) 109 | .join(' | ')};\n` 110 | 111 | const carrierPath = join(dataBasePath, 'carrier') 112 | const { locales: carrierLocales } = await preparePath(carrierPath, 'carrier') 113 | generatedTypes += `export type CarrierLocale = ${carrierLocales 114 | .map((l) => `'${l}'`) 115 | .join(' | ')};\n` 116 | 117 | await prepareTimezones() 118 | 119 | console.log('Creating types...') 120 | writeFileSync(join(__dirname, '/../src/locales.ts'), generatedTypes) 121 | } 122 | 123 | prepare() 124 | .then() 125 | .catch((e) => { 126 | console.error(`error`, e) 127 | }) 128 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.3.0 4 | - Update dependencies 5 | 6 | ## v1.2.15 7 | - Update dependencies 8 | 9 | ## v1.2.14 10 | - Update dependencies 11 | 12 | ## v1.2.13 13 | - Update dependencies 14 | - Refactor tests 15 | 16 | ## v1.2.12 17 | - Update dependencies 18 | 19 | ## v1.2.11 20 | - Update license from MIT to BSL-1.1 21 | - Update dependencies 22 | 23 | ## v1.2.10 24 | - Update dependencies 25 | 26 | ## v1.2.9 27 | - Update readme and tests 28 | 29 | ## v1.2.8 30 | - Update dependencies 31 | - Release to npm 32 | 33 | ## v1.3.3 34 | - Update dependencies 35 | 36 | ## v1.3.4 37 | - Update package.json 38 | 39 | ## v1.3.5 40 | - Fix version 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Business Source License 1.1 2 | ===================== 3 | Business Source License 1.1 https://mariadb.com/bsl11/ 4 | 5 | Licensor: DEV.ME, Ltd. 6 | 7 | Licensed Work: Phone Number Validator 8 | 9 | Additional Use Grant: None 10 | 11 | Copyright 2015-present DEV.ME, Ltd. 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Phone Number information lookup, validation, carrier name, geo and timezone infos 2 | 3 | [![NPM version](https://badgen.net/npm/v/@devmehq/phone-number-validator-js)](https://npm.im/@devmehq/phone-number-validator-js) 4 | [![Build Status](https://github.com/devmehq/phone-number-validator-js/workflows/CI/badge.svg)](https://github.com/devmehq/phone-number-validator-js/actions) 5 | [![Downloads](https://img.shields.io/npm/dm/@devmehq/phone-number-validator-js.svg)](https://www.npmjs.com/package/phone-number-validator-js) 6 | [![UNPKG](https://img.shields.io/badge/UNPKG-OK-179BD7.svg)](https://unpkg.com/browse/@devmehq/phone-number-validator-js@latest/) 7 | 8 | ### Verify phone number, validate format, checking carrier name, geo and timezone infos. 9 | 10 | > This library includes phone number lookup and validation, and the geocoding, carrier mapping and timezone mapping functionalities that are available in some of googles [libphonenumber](https://github.com/google/libphonenumber) libraries. 11 | > 12 | > To reduce the amount of data that needs to be loaded to geocode / carrier map a phone-number for each mapping only the relevant number prefixes are loaded from a binary json file (BSON). 13 | When the prefix could not be found in the provided locale the library tries to fall back to `en` as locale. 14 | > 15 | > The library supports Node.js only at the moment. 16 | 17 | 18 | ## Features 19 | ✅ Check phone number validity 20 | 21 | ✅ Check phone number format 22 | 23 | ✅ Check phone number carrier name 24 | 25 | ✅ Check phone number geolocation (city) 26 | 27 | ✅ Check phone number timezone 28 | 29 | ✅ Check phone number country code 30 | 31 | 32 | ## Use cases 33 | - Increase delivery rate of SMS campaigns by removing invalid phone numbers 34 | - Increase SMS open rate and your marketing IPs reputation 35 | - Protect your website from spam, bots and fake phone numbers 36 | - Protect your product signup form from fake phone numbers 37 | - Protect your website forms from fake phone numbers 38 | - Protect your self from fraud orders and accounts using fake phone numbers 39 | - Integrate phone number verification into your website forms 40 | - Integrate phone number verification into your backoffice administration and order processing 41 | - Integrate phone number verification into your mobile apps 42 | 43 | ## API / Cloud Hosted Service 44 | We offer this `phone verification and validation and more advanced features` in our Scalable Cloud API Service Offering - You could try it here [Phone Number Verification](https://dev.me/products/phone) 45 | 46 | --- 47 | 48 | ## License 49 | 50 | phone-number-validator-js licensed under [Business Source License 1.1](LICENSE). 51 | The BSL allows use only for non-production purposes. 52 | 53 | | Use Case | Is a commercial license required?| 54 | |----------|-----------| 55 | | Exploring phone-number-validator-js for your own research, hobbies, and testing purposes | **No** | 56 | | Using phone-number-validator-js to build a proof-of-concept application | **No** | 57 | | Using phone-number-validator-js to build revenue-generating applications | **Yes** | 58 | | Using phone-number-validator-js to build software that is provided as a service (SaaS) | **Yes** | 59 | | Forking phone-number-validator-js for any production purposes | **Yes** | 60 | 61 | To purchase a license for uses not authorized by BSL, please contact us at [sales@dev.me](mailto:sales@dev.me?subject=Interested%20in%20phone-number-validator-js%20commercial%20license). 62 | 63 | --- 64 | 65 | ## installation and usage instructions 66 | 67 | ## Installation 68 | 69 | ```sh 70 | npm install @devmehq/phone-number-validator-js 71 | ``` 72 | 73 | or 74 | 75 | ```sh 76 | yarn add @devmehq/phone-number-validator-js 77 | ``` 78 | 79 | ## Usage 80 | 81 | The available methods are: 82 | 83 | - `geocoder(phonenumber: PhoneNumber, locale?: GeocoderLocale = 'en'): string | null` - Resolved to the geocode or null if no geocode could be found (e.g. for mobile numbers) 84 | - `carrier(phonenumber: PhoneNumber, locale?: CarrierLocale = 'en'): string | null` - Resolves to the carrier or null if non could be found (e.g. for fixed line numbers) 85 | - `timezones(phonenumber: PhoneNumber): Array | null` - Resolved to an array of timezones or null if non where found. 86 | 87 | ## Examples 88 | 89 | ```js 90 | import { geocoder, carrier, timezones, parsePhoneNumberFromString } from '@devmehq/phone-number-validator-js' 91 | 92 | const fixedLineNumber = parsePhoneNumberFromString('+41431234567') 93 | const locationEN = geocoder(fixedLineNumber) // Zurich 94 | const locationDE = geocoder(fixedLineNumber, 'de') // Zürich 95 | const locationIT = geocoder(fixedLineNumber, 'it') // Zurigo 96 | 97 | const mobileNumber = parsePhoneNumberFromString('+8619912345678') 98 | const carrierEN = carrier(mobileNumber) // China Telecom 99 | const carrierZH = carrier(mobileNumber, 'zh') // 中国电信 100 | 101 | const fixedLineNumber2 = parsePhoneNumberFromString('+49301234567') 102 | const tzones = timezones(fixedLineNumber2) // ['Europe/Berlin'] 103 | ``` 104 | 105 | 106 | ## Testing 107 | ```bash 108 | yarn test 109 | ``` 110 | 111 | ## Contributing 112 | Please feel free to open an issue or create a pull request and fix bugs or add features, All contributions are welcome. Thank you! 113 | 114 | ## LICENSE [Business Source License 1.1](LICENSE.md) 115 | -------------------------------------------------------------------------------- /__tests__/carrier.test.ts: -------------------------------------------------------------------------------- 1 | import { carrier } from '../src' 2 | import parsePhoneNumber from 'libphonenumber-js' 3 | 4 | describe('Phone Number Lookup', () => { 5 | it('should return carrier', async () => { 6 | const phoneNumber = parsePhoneNumber('+14158586273') 7 | const res = carrier(phoneNumber) 8 | expect(res).toEqual(null) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | carrier, 3 | geocoder, 4 | parsePhoneNumberFromString, 5 | timezones, 6 | } from '../src' 7 | 8 | it('geocodes with default locale en', async () => { 9 | const phoneNr = parsePhoneNumberFromString('+41431234567') 10 | const location = geocoder(phoneNr) 11 | expect(location).toEqual('Zurich') 12 | }) 13 | 14 | it('geocodes other locales correctly', async () => { 15 | const phoneNr = parsePhoneNumberFromString('+41431234567') 16 | const locationDE = geocoder(phoneNr, 'de') 17 | expect(locationDE).toEqual('Zürich') 18 | const locationIT = geocoder(phoneNr, 'it') 19 | expect(locationIT).toEqual('Zurigo') 20 | }) 21 | 22 | it('maps a carrier correctly', async () => { 23 | const phoneNr = parsePhoneNumberFromString('01701234567', 'DE') 24 | const carrierEN = carrier(phoneNr) 25 | expect(carrierEN).toEqual('T-Mobile') 26 | // There should not be an arabic mapping for german carrier numbers (I guess?) 27 | const carrierAR = carrier(phoneNr, 'ar') 28 | expect(carrierAR).toEqual('T-Mobile') 29 | }) 30 | 31 | it('maps carriers with different locales correctly', async () => { 32 | const phoneNr = parsePhoneNumberFromString('+8619912345678') 33 | const carrierEN = carrier(phoneNr) 34 | expect(carrierEN).toEqual('China Telecom') 35 | const carrierZH = carrier(phoneNr, 'zh') 36 | expect(carrierZH).toEqual('中国电信') 37 | }) 38 | 39 | it('maps timezones correctly', async () => { 40 | const phoneNr1 = parsePhoneNumberFromString('+49301234567') 41 | const tz1 = timezones(phoneNr1) 42 | expect(tz1).toEqual(['Europe/Berlin']) 43 | 44 | const phoneNr2 = parsePhoneNumberFromString('646-273-5246', 'US') 45 | const tz2 = timezones(phoneNr2) 46 | expect(tz2).toContain('America/New_York') 47 | }) 48 | 49 | it('maps issue #7 to the correct carrier', async () => { 50 | const phoneNr = parsePhoneNumberFromString('+420779990001') 51 | const carrierCZ = carrier(phoneNr) 52 | expect(carrierCZ).toContain('T-Mobile') 53 | }) 54 | 55 | it('maps issue #8 to the correct timezone', async () => { 56 | const phoneNr = parsePhoneNumberFromString('+19168085888') 57 | const tzs = timezones(phoneNr) 58 | expect(tzs).toEqual(['America/Los_Angeles']) 59 | }) 60 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@devmehq/phone-number-validator-js", 3 | "version": "1.3.5", 4 | "description": "Verify phone number, validate format, checking carrier name, geo and timezone infos.", 5 | "keywords": [ 6 | "telephone", 7 | "phone", 8 | "number", 9 | "geo", 10 | "geocode", 11 | "location", 12 | "carrier", 13 | "timezone", 14 | "timezones", 15 | "international", 16 | "libphonenumber" 17 | ], 18 | "homepage": "https://github.com/devmehq/phone-number-validator-js#readme", 19 | "bugs": { 20 | "url": "https://github.com/devmehq/phone-number-validator-js/issues" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/devmehq/phone-number-validator-js.git" 25 | }, 26 | "license": "BSL 1.1", 27 | "author": "DEV.ME (https://dev.me)", 28 | "main": "lib/index.js", 29 | "module": "lib/index.es.js", 30 | "types": "lib/index.d.ts", 31 | "files": [ 32 | "lib", 33 | "resources" 34 | ], 35 | "scripts": { 36 | "build": "rm -rf lib && rollup -c --bundleConfigAsCjs", 37 | "preparemetadata": "rm -rf resources && node .scripts/prepare.js && rm -rf resources/libphonenumber", 38 | "prepublishOnly": "yarn build", 39 | "prettier": "prettier --write \\\"src/**/*.ts\\\" \\\"__tests__/**/*.ts\\\"", 40 | "test": "jest", 41 | "watch": "rm -rf lib && rollup -cw" 42 | }, 43 | "dependencies": { 44 | "bson": "^6.10.2", 45 | "libphonenumber-js": "^1.11.19" 46 | }, 47 | "devDependencies": { 48 | "@release-it/conventional-changelog": "^10.0.0", 49 | "@release-it/keep-a-changelog": "^6.0.0", 50 | "@types/bson": "^4.2.4", 51 | "@types/jest": "^29.5.14", 52 | "@types/node": "^22.13.1", 53 | "@types/shelljs": "^0.8.15", 54 | "jest": "^29.7.0", 55 | "prettier": "^3.5.0", 56 | "release-it": "^18.1.2", 57 | "rollup": "^4.34.6", 58 | "rollup-plugin-typescript2": "^0.36.0", 59 | "shelljs": "^0.8.5", 60 | "ts-jest": "^29.2.5", 61 | "tslib": "^2.8.1", 62 | "typescript": "^5.7.3" 63 | }, 64 | "packageManager": "yarn@1.22.22+sha256.c17d3797fb9a9115bf375e31bfd30058cac6bc9c3b8807a3d8cb2094794b51ca" 65 | } 66 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | // https://semantic-release.gitbook.io/semantic-release/usage/configuration 2 | const pkg = require('./package.json') 3 | const branch = process.env.BRANCH || process.env.CI_REF_NAME || '' 4 | const branchSlug = branch.replace(/\//g, '-') 5 | const branchPrefix = branch.split('/')[0] 6 | 7 | const isMaster = branch === 'master' || branch === 'main' 8 | // semantic-release configuration 9 | module.exports = { 10 | branches: [ 11 | { 12 | name: 'master', 13 | prerelease: false, 14 | }, 15 | { 16 | name: 'main', 17 | prerelease: false, 18 | }, 19 | { 20 | name: 'next', 21 | prerelease: 'next', 22 | }, 23 | { 24 | name: 'develop', 25 | prerelease: 'beta', 26 | }, 27 | { name: branchSlug, prerelease: 'alpha' }, 28 | { name: `${branchPrefix}/**`, prerelease: 'alpha' }, 29 | ], 30 | plugins: [ 31 | [ 32 | '@semantic-release/commit-analyzer', 33 | { 34 | preset: 'angular', 35 | releaseRules: [ 36 | { type: 'breaking', release: 'major' }, 37 | { type: 'feat', release: 'minor' }, 38 | { type: 'fix', release: 'patch' }, 39 | { type: 'revert', release: 'patch' }, 40 | { type: 'docs', release: 'patch' }, 41 | { type: 'refactor', release: 'patch' }, 42 | { type: 'style', release: 'patch' }, 43 | { type: 'test', release: 'patch' }, 44 | { type: 'chore', release: 'patch' }, 45 | { type: 'ci', release: 'patch' }, 46 | { type: 'perf', release: 'patch' }, 47 | { type: 'build', release: 'patch' }, 48 | ], 49 | }, 50 | ], 51 | ['@semantic-release/release-notes-generator'], 52 | // https://github.com/semantic-release/npm 53 | ['@semantic-release/npm'], 54 | // https://github.com/semantic-release/github 55 | [ 56 | '@semantic-release/github', 57 | { 58 | successComment: false, 59 | failComment: false, 60 | }, 61 | ], 62 | // https://github.com/semantic-release/git 63 | isMaster && [ 64 | '@semantic-release/git', 65 | { 66 | assets: [ 67 | 'package.json', 68 | 'package-lock.json', 69 | 'yarn.lock', 70 | 'npm-shrinkwrap.json', 71 | 'CHANGELOG.md', 72 | ], 73 | message: 74 | 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', 75 | GIT_AUTHOR_NAME: pkg.author.name, 76 | GIT_AUTHOR_EMAIL: pkg.author.email, 77 | GIT_COMMITTER_NAME: pkg.author.name, 78 | GIT_COMMITTER_EMAIL: pkg.author.email, 79 | }, 80 | ], 81 | ].filter(Boolean), 82 | } 83 | -------------------------------------------------------------------------------- /release.mjs: -------------------------------------------------------------------------------- 1 | import release from 'release-it' 2 | 3 | const branch = process.env.BRANCH || process.env.CI_REF_NAME || '' 4 | const branchSlug = branch.replace(/\//g, '-') 5 | const branchPrefix = branch.split('/')[0] 6 | 7 | const config = { 8 | isPreRelease: branch !== 'master', 9 | preRelease: branch !== 'master', 10 | preReleaseId: branch === 'master' ? '' : branch, 11 | plugins: { 12 | '@release-it/conventional-changelog': { 13 | preset: { 14 | name: 'conventionalcommits', 15 | types: [ 16 | { type: 'breaking', release: 'major' }, 17 | { type: 'feat', release: 'minor' }, 18 | // match anything else 19 | { type: '**', release: 'patch' }, 20 | { subject: '**', release: 'patch' }, 21 | { message: '**', release: 'patch' }, 22 | ], 23 | }, 24 | }, 25 | }, 26 | } 27 | 28 | console.debug('config', config) 29 | 30 | release(config).then((output) => { 31 | console.debug('output', output) 32 | 33 | const { version: nextVersion, latestVersion, name, changelog } = output || {} 34 | 35 | console.info( 36 | `Last release is ${latestVersion} and next release is ${nextVersion}` 37 | ) 38 | if (latestVersion === nextVersion) { 39 | console.info( 40 | `No release is needed, last release is ${latestVersion} and next release is ${nextVersion}` 41 | ) 42 | return 43 | } 44 | 45 | if (!nextVersion) { 46 | console.info(`No release is needed - no next version`) 47 | } 48 | }) 49 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "rollup-plugin-typescript2"; 2 | import pkg from "./package.json"; 3 | 4 | export default { 5 | input: "src/index.ts", 6 | output: [ 7 | { 8 | file: pkg.main, 9 | format: "cjs" 10 | }, 11 | { 12 | file: pkg.module, 13 | format: "es" 14 | } 15 | ], 16 | external: [ 17 | ...Object.keys(pkg.dependencies || {}), 18 | ...Object.keys(pkg.peerDependencies || {}) 19 | ], 20 | plugins: [typescript()] 21 | }; 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'libphonenumber-js' 2 | import { PhoneNumber } from 'libphonenumber-js' 3 | import { CarrierLocale, GeocoderLocale } from './locales' 4 | import { readFileSync } from 'fs' 5 | import { deserialize, Document } from 'bson' 6 | import { join } from 'path' 7 | 8 | const codeData: Record = {} 9 | 10 | /** 11 | * Maps the dataPath and prefix to geocode, carrier, timezones or null if this info could not be extracted 12 | * 13 | * **Note:** Timezones are returned as single string joined with `&` 14 | * 15 | * @param dataPath Path of the metadata bson file to use 16 | * @param nationalNumber The national (significant) number without whitespaces e.g. `2133734253` 17 | */ 18 | function getCode(dataPath: string, nationalNumber: string) { 19 | try { 20 | // 21 | if (!codeData[dataPath]) { 22 | const bData = readFileSync(dataPath) 23 | codeData[dataPath] = deserialize(bData) 24 | } 25 | // 26 | const data = codeData[dataPath] 27 | let prefix = nationalNumber 28 | // Find the longest match 29 | while (prefix.length > 0) { 30 | const description = data[prefix] 31 | if (description) { 32 | return description as string 33 | } 34 | // Remove a character from the end 35 | prefix = prefix.substring(0, prefix.length - 1) 36 | } 37 | } catch (err) { 38 | // console.log('Could not parse bson', err) 39 | } 40 | return null 41 | } 42 | 43 | /** 44 | * Provides geographical information related to the phone number 45 | * 46 | * @param phonenumber The phone number 47 | * @param locale The preferred locale to use (falls back to `en` if there are no localized carrier infos for the given locale) 48 | */ 49 | export function geocoder( 50 | phonenumber: PhoneNumber | undefined, 51 | locale: GeocoderLocale = 'en' 52 | ) { 53 | const nationalNumber = phonenumber?.nationalNumber.toString() 54 | const countryCallingCode = phonenumber?.countryCallingCode.toString() 55 | if (!nationalNumber || !countryCallingCode) { 56 | return null 57 | } 58 | let dataPath = join( 59 | __dirname, 60 | '../resources/geocodes/', 61 | locale, 62 | `${countryCallingCode}.bson` 63 | ) 64 | // const code = await getCode(dataPath, prefix) 65 | const code = getCode(dataPath, nationalNumber) 66 | if (code) { 67 | return code 68 | } 69 | if (locale !== 'en') { 70 | // Try fallback to english 71 | dataPath = join( 72 | __dirname, 73 | '../resources/geocodes/', 74 | 'en', 75 | `${countryCallingCode}.bson` 76 | ) 77 | // return await getCode(dataPath, prefix) 78 | return getCode(dataPath, nationalNumber) 79 | } 80 | return null 81 | } 82 | 83 | /** 84 | * Maps the phone number to the original carrier 85 | * 86 | * **Note:** This method cannot provide data about the current carrier of the phone number, 87 | * only the original carrier who is assigned to the corresponding range. 88 | * @see https://github.com/google/libphonenumber#mapping-phone-numbers-to-original-carriers 89 | * 90 | * @param phonenumber The phone number 91 | * @param locale The preferred locale to use (falls back to `en` if there are no localized carrier infos for the given locale) 92 | */ 93 | export function carrier( 94 | phonenumber: PhoneNumber | undefined, 95 | locale: CarrierLocale = 'en' 96 | ) { 97 | if (!phonenumber) { 98 | return null 99 | } 100 | const nationalNumber = phonenumber?.nationalNumber.toString() 101 | const countryCallingCode = phonenumber?.countryCallingCode.toString() 102 | if (!nationalNumber || !countryCallingCode) { 103 | return null 104 | } 105 | let dataPath = join( 106 | __dirname, 107 | '../resources/carrier/', 108 | locale, 109 | `${countryCallingCode}.bson` 110 | ) 111 | // const code = await getCode(dataPath, prefix) 112 | const code = getCode(dataPath, nationalNumber) 113 | if (code) { 114 | return code 115 | } 116 | if (locale !== 'en') { 117 | // Try fallback to english 118 | dataPath = join( 119 | __dirname, 120 | '../resources/carrier/', 121 | 'en', 122 | `${countryCallingCode}.bson` 123 | ) 124 | // return await getCode(dataPath, prefix) 125 | return getCode(dataPath, nationalNumber) 126 | } 127 | return null 128 | } 129 | 130 | /** 131 | * Provides all timezones related to the phone number 132 | * @param phonenumber The phone number 133 | */ 134 | export function timezones(phonenumber: PhoneNumber | undefined) { 135 | let nr = phonenumber?.number.toString() 136 | if (!nr) { 137 | return null 138 | } 139 | nr = nr.replace(/^\+/, '') 140 | let dataPath = join(__dirname, '../resources/timezones.bson') 141 | const zones = getCode(dataPath, nr) 142 | if (typeof zones === 'string') { 143 | return zones.split('&') 144 | } 145 | return null 146 | } 147 | -------------------------------------------------------------------------------- /src/locales.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE IS AUTOGENERATED. */ 2 | export type GeocoderLocale = 'ar' | 'be' | 'bg' | 'bs' | 'de' | 'el' | 'en' | 'es' | 'fa' | 'fi' | 'fr' | 'hr' | 'hu' | 'hy' | 'id' | 'it' | 'iw' | 'ja' | 'ko' | 'nl' | 'pl' | 'pt' | 'ro' | 'ru' | 'sq' | 'sr' | 'sv' | 'th' | 'tr' | 'uk' | 'vi' | 'zh' | 'zh_Hant'; 3 | export type CarrierLocale = 'ar' | 'be' | 'en' | 'fa' | 'ko' | 'ru' | 'uk' | 'zh' | 'zh_Hant'; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "esnext", 5 | "lib": [ 6 | "ESNext" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "moduleResolution": "node", 12 | "types": [ 13 | "node", 14 | "jest" 15 | ], 16 | "esModuleInterop": true, 17 | "downlevelIteration": true 18 | }, 19 | "include": [ 20 | "src/**/*" 21 | ], 22 | "exclude": [ 23 | "node_modules", 24 | "**/*.test.ts" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------