phoneshield
v1.3.0
Published
A lightning-fast, privacy-first phone intelligence engine. Smarter validation, zero-knowledge lookups, and built-in fraud detection.
Maintainers
Readme
PhoneShield
A lightning-fast, privacy-first phone intelligence engine. A smarter, tree-shakable alternative to libphonenumber-js and numverify — works everywhere JavaScript runs: Node.js, browsers, React, Vue, Svelte, plain HTML.
Features
- < 5KB per country — ESM-first, tree-shakable. Import only what you need.
- Framework-agnostic — Core engine has zero dependencies. Use it in Node, Deno, Bun, or any frontend framework.
- Hybrid validation — Local regex + length checks, no network calls required.
- "Did You Mean?" — Suggests corrections when a number is off by 1–2 digits.
- Intelligence schema — Returns
isValid, E.164format,country,lineType, andriskScore. - Privacy-first — SHA-256 hashing for zero-knowledge spam database lookups.
- Real-time formatting — Built-in stateful formatter with debounce (framework-agnostic).
- Optional React hook —
usePhoneShieldavailable viaphoneshield/react(React is never required). - TypeScript native — Full type safety and IntelliSense out of the box.
Installation
npm install phoneshieldQuick Start
Country-Specific Validation (Tree-Shakable)
Import only the country you need — each entry point pulls in just that country's metadata:
import { validateUS } from 'phoneshield/us';
const result = validateUS('(202) 555-1234');
// {
// isValid: true,
// format: '+12025551234',
// country: 'US',
// lineType: 'Mobile',
// riskScore: 0.0
// }Multi-Country
import { validateUS } from 'phoneshield/us';
import { validateUK } from 'phoneshield/uk';
import { validateFR } from 'phoneshield/fr';
import { validateIN } from 'phoneshield/in';
validateUS('2025551234');
validateUK('7400123456');
validateFR('612345678');
validateIN('9876543210');Generic Validation (Any Country)
import { validate, getMetadata } from 'phoneshield';
const metadata = getMetadata('DE');
const result = validate('15112345678', metadata, {
enableSuggestions: true,
});Supported Countries
| Code | Country | Dial Code | Import Path |
| ---- | ------- | --------- | ----------- |
| US | United States | +1 | phoneshield/us |
| CA | Canada | +1 | phoneshield/ca |
| UK | United Kingdom | +44 | phoneshield/uk |
| AU | Australia | +61 | phoneshield/au |
| DE | Germany | +49 | phoneshield/de |
| FR | France | +33 | phoneshield/fr |
| JP | Japan | +81 | phoneshield/jp |
| IN | India | +91 | phoneshield/in |
Validation Result Schema
Every validation call returns a ValidationResult:
interface ValidationResult {
isValid: boolean; // Pass/fail
format: string | null; // E.164 format (e.g. "+12025551234")
country: CountryCode | null; // "US", "UK", "FR", etc.
lineType: LineType; // "Mobile" | "Landline" | "VoIP" | "TollFree" | "Premium" | "Unknown"
riskScore: number; // 0.0 (safe) to 1.0 (high risk)
suggestion?: string; // "Did you mean (202) 555-1234?"
errors?: string[]; // Human-readable error messages
}"Did You Mean?" Engine
When a number is off by 1–2 digits, PhoneShield suggests the closest valid number:
import { validateUS } from 'phoneshield/us';
const result = validateUS('202555123'); // 9 digits instead of 10
console.log(result.isValid); // false
console.log(result.suggestion); // "(202) 555-1234"Risk Scoring
PhoneShield scores numbers from 0.0 (safe) to 1.0 (high risk) based on:
- Invalid format/length — +0.5
- Premium numbers (e.g. 900) — +0.3
- VoIP numbers — +0.2
- Unknown line type — +0.25
- Repeating digits (e.g. 5555555) — +0.15
- Sequential patterns (e.g. 123456) — +0.1
- Suspicious prefixes (000, 999) — +0.2
import { validateUS } from 'phoneshield/us';
const result = validateUS('9001234567');
console.log(result.riskScore); // 0.3 (Premium number)
console.log(result.lineType); // "Premium"Line Type Detection
import { validateUS } from 'phoneshield/us';
validateUS('2025551234').lineType; // "Mobile" or "Landline"
validateUS('8005551234').lineType; // "TollFree"
validateUS('9005551234').lineType; // "Premium"Privacy-First Hashing
Generate SHA-256 hashes to query spam databases without exposing the actual number:
import { hashPhoneNumber } from 'phoneshield';
const hash = await hashPhoneNumber('+12025551234');
// "a3f2b8c1..." — send this to your spam API, not the raw numberUses the Web Crypto API (browser + Node 18+). No external dependencies.
Real-Time Formatting (Framework-Agnostic)
The createPhoneFormatter() engine works in any JavaScript environment — no React, no framework needed:
import { createPhoneFormatter } from 'phoneshield';
const formatter = createPhoneFormatter('US', {
debounceMs: 300,
enableSuggestions: true,
onStateChange: (state) => {
console.log(state.formattedValue); // "(202) 555-1234"
console.log(state.validation); // ValidationResult or null
console.log(state.isValidating); // true/false
},
});
// Feed input as the user types
formatter.handleInput('2');
formatter.handleInput('20');
formatter.handleInput('202');
formatter.handleInput('2025551234');
// Read state at any time
const state = formatter.getState();
// Clean up
formatter.clear();
formatter.destroy();Usage with Vue
import { ref, onMounted, onUnmounted } from 'vue';
import { createPhoneFormatter } from 'phoneshield';
const formattedValue = ref('');
const validation = ref(null);
let formatter;
onMounted(() => {
formatter = createPhoneFormatter('FR', {
debounceMs: 250,
onStateChange: (state) => {
formattedValue.value = state.formattedValue;
validation.value = state.validation;
},
});
});
onUnmounted(() => formatter?.destroy());
function onInput(e) {
formatter?.handleInput(e.target.value);
}Usage with Svelte
import { createPhoneFormatter } from 'phoneshield';
import { onDestroy } from 'svelte';
let formattedValue = '';
let validation = null;
const formatter = createPhoneFormatter('DE', {
onStateChange: (state) => {
formattedValue = state.formattedValue;
validation = state.validation;
},
});
onDestroy(() => formatter.destroy());
function handleInput(e) {
formatter.handleInput(e.target.value);
}Usage in Node.js / Backend
import { validateUS } from 'phoneshield/us';
import { hashPhoneNumber } from 'phoneshield';
// Validate incoming phone number
const result = validateUS(req.body.phone);
if (!result.isValid) {
return res.status(400).json({ errors: result.errors });
}
// Store only the hash
const hash = await hashPhoneNumber(result.format);
await db.users.update({ phoneHash: hash });React Hook (Optional)
Install React as usual — it's an optional peer dependency. Import from the dedicated subpath:
import { usePhoneShield } from 'phoneshield/react';
function PhoneInput() {
const {
formattedValue,
validation,
isValidating,
handleChange,
clear,
} = usePhoneShield('US', { debounceMs: 300, enableSuggestions: true });
return (
<div>
<input
type="tel"
value={formattedValue}
onChange={(e) => handleChange(e.target.value)}
placeholder="(555) 123-4567"
/>
{isValidating && <span>Validating...</span>}
{validation && (
<div>
<p>Valid: {validation.isValid ? 'Yes' : 'No'}</p>
<p>Type: {validation.lineType}</p>
<p>Risk: {(validation.riskScore * 100).toFixed(0)}%</p>
{validation.suggestion && (
<p>Did you mean: {validation.suggestion}?</p>
)}
</div>
)}
<button onClick={clear}>Clear</button>
</div>
);
}Formatting Utilities
import { normalizePhoneNumber, formatPhoneNumber, toE164 } from 'phoneshield';
import { US_METADATA } from 'phoneshield/us';
normalizePhoneNumber('(202) 555-1234');
// "2025551234"
formatPhoneNumber('2025551234', US_METADATA);
// "(202) 555-1234"
toE164('2025551234', US_METADATA);
// "+12025551234"Custom Country Metadata
Add your own country by implementing the CountryMetadata interface:
import { CountryMetadata, validate } from 'phoneshield';
const MY_METADATA: CountryMetadata = {
countryCode: 'MY' as any,
dialCode: '+60',
patterns: {
mobile: [/^1[0-46-9]\d{7,8}$/],
landline: [/^[3-9]\d{7}$/],
voip: [],
tollFree: [/^1800\d{6}$/],
premium: [],
},
lengths: [9, 10],
format: (digits) =>
digits.length === 10
? `${digits.slice(0, 3)}-${digits.slice(3, 6)} ${digits.slice(6)}`
: digits,
};
const result = validate('123456789', MY_METADATA);API Reference
Core
| Function | Description |
| -------- | ----------- |
| validate(input, metadata, options?) | Full validation with intelligence schema |
| validateUS(phone, options?) | US-specific (tree-shakable) |
| validateUK(phone, options?) | UK-specific |
| validateCA(phone, options?) | Canada-specific |
| validateAU(phone, options?) | Australia-specific |
| validateDE(phone, options?) | Germany-specific |
| validateFR(phone, options?) | France-specific |
| validateJP(phone, options?) | Japan-specific |
| validateIN(phone, options?) | India-specific |
| getMetadata(country) | Get metadata for a country code |
Formatting
| Function | Description |
| -------- | ----------- |
| normalizePhoneNumber(input) | Strip all non-digit characters |
| formatPhoneNumber(digits, metadata) | Format to local display format |
| toE164(digits, metadata) | Format to E.164 international format |
Real-Time Formatter
| Function | Description |
| -------- | ----------- |
| createPhoneFormatter(country?, options?) | Create a stateful formatter instance |
Returns a PhoneFormatter with:
handleInput(input)— Process new inputgetState()— Get current{ value, formattedValue, validation, isValidating }clear()— Reset statedestroy()— Clean up timers
Privacy
| Function | Description |
| -------- | ----------- |
| hashPhoneNumber(phone) | SHA-256 hash (async, returns hex string) |
| createZKProof(phone) | Alias for hashPhoneNumber |
React (Optional)
import { usePhoneShield } from 'phoneshield/react';| Hook | Description |
| ---- | ----------- |
| usePhoneShield(country?, options?) | Real-time formatting + validation hook |
Returns { value, formattedValue, validation, isValidating, handleChange, clear }.
Options
interface PhoneShieldOptions {
defaultCountry?: CountryCode; // Fallback country
strictMode?: boolean; // Stricter pattern matching
enableSuggestions?: boolean; // Enable "Did You Mean?" (default: true)
}For createPhoneFormatter and usePhoneShield, you can also pass:
debounceMs— Debounce delay in ms (default:300)
Bundle Size
| Import | Size |
| ------ | ---- |
| phoneshield/us | ~1.2 KB |
| phoneshield/fr | ~1.0 KB |
| Any single country | < 2 KB |
| phoneshield (all countries) | ~3.5 KB |
| phoneshield/react | ~15 KB (includes core) |
Measured with tsup tree-shaking enabled. Actual sizes depend on your bundler.
Compatibility
- Node.js 18+ (uses
crypto.subtlefor hashing) - Browsers: Chrome 37+, Firefox 34+, Safari 11+, Edge 79+
- Deno, Bun — works out of the box
- React 17+ (optional, for
phoneshield/reactonly)
Contributing
We welcome contributions! Here's how to get started:
Development Setup
Fork and clone the repository
git clone https://github.com/youssefbrr/PhoneShield.git cd PhoneShieldInstall dependencies
npm installBuild the package
npm run buildRun tests
npm test
Project Structure
src/
├── core/ # Core validation engine
├── countries/ # Country-specific metadata
├── formatters/ # Phone number formatting utilities
├── privacy/ # Hashing and privacy features
└── react/ # React hooks (optional)Adding a New Country
- Create metadata file in
src/countries/[country].ts - Define patterns for mobile, landline, VoIP, toll-free, and premium numbers
- Add formatting function
- Export from
src/countries/index.ts - Add tests
- Update README with new country
Making Changes
- Create a feature branch:
git checkout -b feature/your-feature - Make your changes
- Add tests for new functionality
- Ensure all tests pass:
npm test - Build to verify:
npm run build - Commit with descriptive message:
git commit -m "feat: add feature description" - Push and create a Pull Request
Commit Convention
We follow Conventional Commits:
feat:New featurefix:Bug fixdocs:Documentation changestest:Adding or updating testsrefactor:Code refactoringperf:Performance improvementschore:Maintenance tasks
Pull Request Guidelines
- Keep PRs focused on a single feature or fix
- Include tests for new functionality
- Update documentation as needed
- Ensure all tests pass
- Follow existing code style
License
MIT
