From d376f8d4dc0669ed0d4b2f16a9065d43e626e686 Mon Sep 17 00:00:00 2001 From: tjkcc Date: Sat, 3 Oct 2026 10:31:59 -0700 Subject: [PATCH 1/2] Add context7.json for Context7 indexing Title, description and agent-facing rules for the Context7 index (context7.com): phone format, server-side key, string-typed fields, action/deliverable as the recommendation, when to pass ip, add-on credit costs, DNC coverage semantics, retry/429 handling, credits. Co-Authored-By: Claude Fable 5.1 --- context7.json | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 context7.json diff --git a/context7.json b/context7.json new file mode 100644 index 0000000..f33e83f --- /dev/null +++ b/context7.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://context7.com/schema/context7.json", + "projectTitle": "CheckThatPhone Node.js SDK", + "description": "Official Node.js client for the CheckThatPhone phone validation API: US and Canada carrier lookup, line type, portability, deliverability, GeoIP and timezone, TCPA litigator screening and a free state do-not-call scrub in one call.", + "folders": [], + "excludeFolders": ["src", "test", ".github"], + "excludeFiles": ["LICENSE", "package-lock.json"], + "rules": [ + "Phone numbers must be US or Canadian: 10 digits, or 11 digits starting with 1. Strip formatting before calling client.lookup(); anything else is rejected with HTTP 400 and costs nothing.", + "Keep the API key server-side, e.g. new CheckThatPhone(process.env.CHECKTHATPHONE_API_KEY). Never call the API from browser code.", + "Every value in result.data is a string: compare with 'true' / 'false' (result.data.deliverable === 'true'), never with booleans.", + "Use result.data.action and result.data.deliverable as the recommendation for a number. Do not re-derive deliverability from raw carrier fields.", + "Pass options.ip (the contact's IP address) whenever you have it: it upgrades location to city level and sets an IANA timezone (result.data.timezone) at no extra credit. Without it, location falls back to the area code.", + "Add-ons are opt-in flags on the options object: litigatorFilter costs +1 credit per lookup, landlineSmsLookup costs +1 credit only when the number is a landline, dncOther (state DNC registries plus the national complainer list) is free. Enable only the add-ons you read.", + "With dncOther, a blank dncStateResult means clear only when dncStateCovered is 'true'; some states have no registry data, so treat dncStateCovered 'false' as not checked.", + "Catch CheckThatPhoneError and check err.retryable before retrying; a 429 means the per-minute rate limit was hit, so back off. Failed and invalid requests cost 0 credits.", + "One lookup costs one credit plus any add-ons (result.creditsUsed). The free tier is 500 lookups per month and hard-capped, so cache results if you re-check the same numbers often." + ], + "previousVersions": [] +} From 730877cfc86d0018c5972f9920b62bfc2f02316f Mon Sep 17 00:00:00 2001 From: tjkcc Date: Sat, 3 Oct 2026 10:45:29 -0700 Subject: [PATCH 2/2] Add dncState / dncComplainer; deprecate dncOther; 0.2.0 The API split the free DNC scrub into two independent checks (state registries, national complainer list) and kept dncOther only as a deprecated alias. The client now exposes both as their own options, translates the old flag into the two new ones on the wire so existing code keeps working, types the new dncStateChecked / dncComplainerChecked fields, and the README states coverage as it is today (38 states plus DC; absent fields mean not checked, never clear). Context7 rules updated to match. Co-Authored-By: Claude Fable 5.1 --- README.md | 19 ++++++++++++------- context7.json | 15 +++++++++++---- package-lock.json | 4 ++-- package.json | 2 +- src/index.ts | 27 ++++++++++++++++++++++----- test/client.test.ts | 18 ++++++++++++++++-- 6 files changed, 64 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 8ceadd8..c3697e6 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![npm version](https://img.shields.io/npm/v/checkthatphone)](https://www.npmjs.com/package/checkthatphone) [![CI](https://github.com/CheckThatPhone/checkthatphone-node/actions/workflows/ci.yml/badge.svg)](https://github.com/CheckThatPhone/checkthatphone-node/actions/workflows/ci.yml) [![node >= 18](https://img.shields.io/node/v/checkthatphone)](https://www.npmjs.com/package/checkthatphone) -Official Node.js client for the [CheckThatPhone](https://checkthatphone.com) phone validation API. Validate US and Canadian phone numbers in real time: carrier and line type from live carrier data, portability and deliverability, GeoIP and timezone, plus optional TCPA litigator screening and a free state do-not-call scrub — one call, one credit. +Official Node.js client for the [CheckThatPhone](https://checkthatphone.com) phone validation API. Validate US and Canadian phone numbers in real time: carrier and line type from live carrier data, portability and deliverability, GeoIP and timezone, plus optional TCPA litigator screening and free state do-not-call and complainer scrubs — one call, one credit. Zero dependencies. Node 18+. TypeScript types included. @@ -41,17 +41,22 @@ if (result.data.litigator === 'true') { } ``` -## State DNC scrub (free) +## State DNC and complainer scrub (free) -Screen state do-not-call registries (40 states) and a national complainer list at no extra credit: +Screen state do-not-call registries (38 states plus DC) and a national complainer list at no extra credit. Each check is its own flag: ```js -const result = await client.lookup('8182925409', { dncOther: true }); -result.data.dncStateResult; // "STATE DNC" or "" -result.data.dncComplainerResult; // "DNC COMPLAINER" or "" -result.data.dncStateCovered; // "false" = state not in the data; don't read "" as clear +const result = await client.lookup('8182925409', { dncState: true, dncComplainer: true }); +result.data.dncStateChecked; // "true" when the state check ran; absent for the 12 states with no registry data +result.data.dncStateResult; // "STATE DNC" on a match, "" otherwise (only when dncStateChecked is "true") +result.data.dncComplainerChecked; // "true" when the complainer check ran +result.data.dncComplainerResult; // "DNC COMPLAINER" on a match, "" otherwise ``` +Read `dncStateChecked` before `dncStateResult`: a number in an uncovered state comes back with neither field, which means not checked, not clear. `"error"` means the check could not complete; treat it as unavailable. + +`dncOther: true` still works as a shorthand for both checks, but it is deprecated. + ## Landline SMS reachability Some landlines can receive texts. Detect them instead of dropping them (+1 credit, charged only when the number is a landline): diff --git a/context7.json b/context7.json index f33e83f..82444ef 100644 --- a/context7.json +++ b/context7.json @@ -3,16 +3,23 @@ "projectTitle": "CheckThatPhone Node.js SDK", "description": "Official Node.js client for the CheckThatPhone phone validation API: US and Canada carrier lookup, line type, portability, deliverability, GeoIP and timezone, TCPA litigator screening and a free state do-not-call scrub in one call.", "folders": [], - "excludeFolders": ["src", "test", ".github"], - "excludeFiles": ["LICENSE", "package-lock.json"], + "excludeFolders": [ + "src", + "test", + ".github" + ], + "excludeFiles": [ + "LICENSE", + "package-lock.json" + ], "rules": [ "Phone numbers must be US or Canadian: 10 digits, or 11 digits starting with 1. Strip formatting before calling client.lookup(); anything else is rejected with HTTP 400 and costs nothing.", "Keep the API key server-side, e.g. new CheckThatPhone(process.env.CHECKTHATPHONE_API_KEY). Never call the API from browser code.", "Every value in result.data is a string: compare with 'true' / 'false' (result.data.deliverable === 'true'), never with booleans.", "Use result.data.action and result.data.deliverable as the recommendation for a number. Do not re-derive deliverability from raw carrier fields.", "Pass options.ip (the contact's IP address) whenever you have it: it upgrades location to city level and sets an IANA timezone (result.data.timezone) at no extra credit. Without it, location falls back to the area code.", - "Add-ons are opt-in flags on the options object: litigatorFilter costs +1 credit per lookup, landlineSmsLookup costs +1 credit only when the number is a landline, dncOther (state DNC registries plus the national complainer list) is free. Enable only the add-ons you read.", - "With dncOther, a blank dncStateResult means clear only when dncStateCovered is 'true'; some states have no registry data, so treat dncStateCovered 'false' as not checked.", + "Add-ons are opt-in flags on the options object: litigatorFilter costs +1 credit per lookup, landlineSmsLookup costs +1 credit only when the number is a landline, dncState (state do-not-call registries) and dncComplainer (national complainer list) are free. Enable only the add-ons you read. dncOther is a deprecated alias for both DNC checks.", + "With dncState, read result.data.dncStateChecked before dncStateResult: both are absent for the 12 states with no registry data (not checked, never clear), and 'error' means the check could not complete.", "Catch CheckThatPhoneError and check err.retryable before retrying; a 429 means the per-minute rate limit was hit, so back off. Failed and invalid requests cost 0 credits.", "One lookup costs one credit plus any add-ons (result.creditsUsed). The free tier is 500 lookups per month and hard-capped, so cache results if you re-check the same numbers often." ], diff --git a/package-lock.json b/package-lock.json index b60c2f5..5213c75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "checkthatphone", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "checkthatphone", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "devDependencies": { "tsup": "^8.0.0", diff --git a/package.json b/package.json index f12b1f5..82299cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "checkthatphone", - "version": "0.1.0", + "version": "0.2.0", "description": "Official Node.js client for the CheckThatPhone phone validation API — US & Canada carrier lookup, line type, TCPA litigator and state DNC screening", "keywords": ["phone-validation", "carrier-lookup", "tcpa", "litigator-scrub", "dnc", "phone-number", "sms", "nanp", "line-type"], "homepage": "https://checkthatphone.com/docs", diff --git a/src/index.ts b/src/index.ts index d825bad..f937b90 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,8 +12,15 @@ export interface LookupOptions { litigatorFilter?: boolean; /** Landline SMS reachability (+1 credit, landlines only). Adds dipMessaging* fields. */ landlineSmsLookup?: boolean; - /** State DNC & complainers scrub (free). Adds dncOtherChecked, dncStateResult, - * dncComplainerResult, dncStateCovered. */ + /** State do-not-call registry check (free; 38 states plus DC). Adds + * dncStateChecked and dncStateResult; both are absent for the 12 states + * with no registry data, so "absent" means not checked, never clear. */ + dncState?: boolean; + /** National complainer-list check (free). Adds dncComplainerChecked and + * dncComplainerResult. */ + dncComplainer?: boolean; + /** @deprecated Use dncState and dncComplainer. Turns both on; the client + * sends the two new flags, never the legacy one. */ dncOther?: boolean; } @@ -57,9 +64,18 @@ export interface LookupData { dipMessagingProvider?: string; dipMessagingRefId?: string; dipMessagingCountryCode?: string; - dncOtherChecked?: string; + /** "true" when the state check ran; "error" when it could not complete + * (treat as unavailable, not clear). Absent for uncovered states. */ + dncStateChecked?: string; + /** "STATE DNC" on a registry match, "" otherwise. Only when dncStateChecked is "true". */ dncStateResult?: string; + /** "true" when the complainer check ran; "error" when it could not complete. */ + dncComplainerChecked?: string; + /** "DNC COMPLAINER" on a match, "" otherwise. Only when dncComplainerChecked is "true". */ dncComplainerResult?: string; + /** Legacy: "true" when either DNC check ran. */ + dncOtherChecked?: string; + /** @deprecated Same answer as dncStateChecked. */ dncStateCovered?: string; [key: string]: unknown; } @@ -126,7 +142,8 @@ export class CheckThatPhone { if (options.ip) body.ip = options.ip; if (options.litigatorFilter) body.litigatorFilter = true; if (options.landlineSmsLookup) body.landlineSmsLookup = true; - if (options.dncOther) body.dncOther = true; + if (options.dncState || options.dncOther) body.dncState = true; + if (options.dncComplainer || options.dncOther) body.dncComplainer = true; const res = await fetch(`${this.baseUrl}/v1/lookup`, { method: 'POST', @@ -157,6 +174,6 @@ export class CheckThatPhone { } } -export const VERSION = '0.1.0'; +export const VERSION = '0.2.0'; export default CheckThatPhone; diff --git a/test/client.test.ts b/test/client.test.ts index 01a4a66..54911f3 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -17,7 +17,7 @@ describe('CheckThatPhone.lookup', () => { data: { subscriber: '8182925409', nanpType: 'mobile', litigator: 'false' }, }); const client = new CheckThatPhone('ctp_live_test'); - const res = await client.lookup('(818) 292-5409', { litigatorFilter: true, dncOther: true }); + const res = await client.lookup('(818) 292-5409', { litigatorFilter: true, dncState: true, dncComplainer: true }); expect(res.success).toBe(true); expect(res.creditsUsed).toBe(2); @@ -26,10 +26,24 @@ describe('CheckThatPhone.lookup', () => { const [url, init] = fn.mock.calls[0] as [string, RequestInit]; expect(url).toBe('https://api.checkthatphone.com/v1/lookup'); const sent = JSON.parse(String(init.body)); - expect(sent).toEqual({ phone: '(818) 292-5409', litigatorFilter: true, dncOther: true }); + expect(sent).toEqual({ phone: '(818) 292-5409', litigatorFilter: true, dncState: true, dncComplainer: true }); expect((init.headers as Record).authorization).toBe('Bearer ctp_live_test'); }); + it('translates the deprecated dncOther flag into dncState + dncComplainer on the wire', async () => { + const fn = mockFetchOnce(200, { success: true, credits_used: 1, data: {} }); + await new CheckThatPhone('k').lookup('8182925409', { dncOther: true }); + const sent = JSON.parse(String((fn.mock.calls[0] as [string, RequestInit])[1].body)); + expect(sent).toEqual({ phone: '8182925409', dncState: true, dncComplainer: true }); + }); + + it('sends only the DNC check that was asked for', async () => { + const fn = mockFetchOnce(200, { success: true, credits_used: 1, data: {} }); + await new CheckThatPhone('k').lookup('8182925409', { dncComplainer: true }); + const sent = JSON.parse(String((fn.mock.calls[0] as [string, RequestInit])[1].body)); + expect(sent).toEqual({ phone: '8182925409', dncComplainer: true }); + }); + it('throws a typed error with status and detail on 4xx', async () => { mockFetchOnce(400, { error: 'Invalid request', detail: 'phone must be 10-11 digits' }); const client = new CheckThatPhone('k');