Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ The format is based on
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.0] - 2026-06-03

### Changed

- `Validator` now carries a `scope` discriminator:
`{ scope: "country"; country: CountryCode }` or
`{ scope: "global" }`. The optional `country?` field
is removed in favor of this discriminated union.
Consumers that read `validator.country` must first
narrow on `validator.scope === "country"`.
`ValidatorScope` is exported from the package root.

## [1.0.0] - 2026-05-17

### Changed
Expand Down Expand Up @@ -57,5 +69,6 @@ and this project adheres to
artifacts.
- Per-identifier entry points for tree-shaking.

[2.0.0]: https://github.com/stella/stdnum/releases/tag/v2.0.0
[1.0.0]: https://github.com/stella/stdnum/releases/tag/v1.0.0
[0.1.0]: https://github.com/stella/stdnum/releases/tag/v0.1.0
39 changes: 25 additions & 14 deletions __test__/invariants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
* Auto-discovers all validators and checks metadata
* consistency: examples exist, examples validate,
* compact is idempotent, entityType is valid,
* required fields are non-empty, directory name
* matches country field, and example lengths match
* the declared `lengths` array.
* required fields are non-empty, scope matches the
* export namespace, and example lengths match the
* declared `lengths` array.
*
* New validators get all checks automatically when
* added to the index.
Expand Down Expand Up @@ -93,22 +93,33 @@ for (const [name, v] of validators) {
});
}

// (c) Directory name matches country field
test("directory name matches country field", () => {
// (c) Scope matches export namespace
test("scope matches export namespace", () => {
const ns = name.split(".")[0]!;
if (INTERNATIONAL_NAMESPACES.has(ns)) {
expect(
v.country,
`${name} is international but has country "${v.country}"`,
).toBeUndefined();
} else {
// Namespace `is_` maps to country "IS"
const expected = ns.replace(/_$/, "").toUpperCase();
v.scope,
`${name}: expected global scope but got "${v.scope}"`,
).toBe("global");
expect(
v.country,
`${name}: expected country "${expected}" but got "${v.country}"`,
).toBe(expected);
"country" in v,
`${name} is global but has a country field`,
).toBe(false);
return;
}

if (v.scope !== "country") {
throw new Error(
`${name}: expected country scope but got "${v.scope}"`,
);
}

// Namespace `is_` maps to country "IS"
const expected = ns.replace(/_$/, "").toUpperCase();
expect(
v.country,
`${name}: expected country "${expected}" but got "${v.country}"`,
).toBe(expected);
});

// (d) Examples validate successfully
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stll/stdnum",
"version": "1.0.0",
"version": "2.0.0",
"description": "Validate, compact, and format standard identifiers for Node.js and Bun. Pure TypeScript, zero dependencies, tree-shakeable per identifier.",
"keywords": [
"credit-card",
Expand Down
8 changes: 6 additions & 2 deletions src/ad/nrt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ const generate = (): string => {
* @see https://www.oecd.org/tax/automatic-exchange/crs-implementation-and-assistance/tax-identification-numbers/Andorra-TIN.pdf
*/

import type { ValidateResult, Validator } from "../types";
import type {
ValidateResult,
CountryValidator,
} from "../types";

import { clean } from "#util/clean";
import {
Expand Down Expand Up @@ -98,12 +101,13 @@ const format = (value: string): string => {
};

/** Andorra NRT (Número de Registre Tributari). */
const nrt: Validator = {
const nrt: CountryValidator<"AD"> = {
name: "Andorra Tax Number",
localName: "Número de Registre Tributari",
abbreviation: "NRT",
aliases: ["NRT", "Número de Registre Tributari"] as const,
candidatePattern: "[A-Z]-?\\d{6}-?[A-Z]",
scope: "country",
country: "AD",
entityType: "any",
sourceUrl:
Expand Down
8 changes: 6 additions & 2 deletions src/ae/eid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
* @see https://u.ae/en/information-and-services/visa-and-emirates-id/emirates-id
*/

import type { ValidateResult, Validator } from "../types";
import type {
ValidateResult,
CountryValidator,
} from "../types";

import {
luhnChecksum,
Expand Down Expand Up @@ -77,12 +80,13 @@ const generate = (): string => {
};

/** UAE Emirates ID. */
const eid: Validator = {
const eid: CountryValidator<"AE"> = {
name: "Emirates ID",
localName: "رقم الهوية",
abbreviation: "EID",
aliases: ["EID", "Emirates ID", "رقم الهوية"] as const,
candidatePattern: "784-\\d{4}-\\d{7}-\\d",
scope: "country",
country: "AE",
entityType: "person",
lengths: [15],
Expand Down
8 changes: 6 additions & 2 deletions src/ai/tin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ const generate = (): string => {
* @see https://www.oecd.org/tax/automatic-exchange/crs-implementation-and-assistance/tax-identification-numbers/Anguilla-TIN.pdf
*/

import type { ValidateResult, Validator } from "../types";
import type {
ValidateResult,
CountryValidator,
} from "../types";

import { clean } from "#util/clean";
import { randomDigits } from "#util/generate";
Expand Down Expand Up @@ -74,12 +77,13 @@ const format = (value: string): string => {
* (format-only validation, no published check
* digit algorithm).
*/
const tin: Validator = {
const tin: CountryValidator<"AI"> = {
name: "Anguilla Tax Identification Number",
localName: "Tax Identification Number",
abbreviation: "TIN",
aliases: ["TIN"] as const,
candidatePattern: "\\d{11}",
scope: "country",
country: "AI",
entityType: "any",
lengths: [10] as const,
Expand Down
8 changes: 6 additions & 2 deletions src/al/nipt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ const generate = (): string => {
* @see https://www.tatime.gov.al/eng/c/4/103/business-lifecycle
*/

import type { ValidateResult, Validator } from "../types";
import type {
ValidateResult,
CountryValidator,
} from "../types";

import { clean } from "#util/clean";
import { randomDigits, randomInt } from "#util/generate";
Expand Down Expand Up @@ -57,13 +60,14 @@ const validate = (value: string): ValidateResult => {
const format = (value: string): string => compact(value);

/** Albanian NIPT (tax identification number). */
const nipt: Validator = {
const nipt: CountryValidator<"AL"> = {
name: "Albanian Tax Number",
localName:
"Numri i Identifikimit për Personin e Tatueshëm",
abbreviation: "NIPT",
aliases: ["NIPT", "NUIS"] as const,
candidatePattern: "[A-Z]\\d{8}[A-Z]",
scope: "country",
country: "AL",
entityType: "any",
sourceUrl: "https://www.tatime.gov.al/",
Expand Down
8 changes: 6 additions & 2 deletions src/am/tin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ const generate = (): string => randomDigits(8);
* @see https://www.src.am/en/taxpayerSearchSystemPage/112
*/

import type { ValidateResult, Validator } from "../types";
import type {
ValidateResult,
CountryValidator,
} from "../types";

import { clean } from "#util/clean";
import { randomDigits } from "#util/generate";
Expand Down Expand Up @@ -49,12 +52,13 @@ const validate = (value: string): ValidateResult => {
const format = (value: string): string => compact(value);

/** Armenian Tax Identification Number. */
const tin: Validator = {
const tin: CountryValidator<"AM"> = {
name: "Armenian Tax ID",
localName: "Հարկ վճարողի հաշվառման համար",
abbreviation: "TIN",
aliases: ["ՀՎՀՀ", "TIN"] as const,
candidatePattern: "\\d{8}",
scope: "country",
country: "AM",
entityType: "any",
lengths: [8] as const,
Expand Down
8 changes: 6 additions & 2 deletions src/ar/cbu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
* @see https://es.wikipedia.org/wiki/Clave_bancaria_uniforme
*/

import type { ValidateResult, Validator } from "../types";
import type {
ValidateResult,
CountryValidator,
} from "../types";

import { clean } from "#util/clean";
import { randomDigits } from "#util/generate";
Expand Down Expand Up @@ -84,12 +87,13 @@ const generate = (): string => {
};

/** Argentine Uniform Bank Key. */
const cbu: Validator = {
const cbu: CountryValidator<"AR"> = {
name: "Argentine Bank Account Number",
localName: "Clave Bancaria Uniforme",
abbreviation: "CBU",
aliases: ["CBU", "Clave Bancaria Uniforme"] as const,
candidatePattern: "\\d{22}",
scope: "country",
country: "AR",
entityType: "any",
sourceUrl:
Expand Down
8 changes: 6 additions & 2 deletions src/ar/cuit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
* @see https://en.wikipedia.org/wiki/CUIT_(Argentina)
*/

import type { ValidateResult, Validator } from "../types";
import type {
ValidateResult,
CountryValidator,
} from "../types";

import { clean } from "#util/clean";
import { randomDigits, randomPick } from "#util/generate";
Expand Down Expand Up @@ -120,7 +123,7 @@ const generate = (): string => {
* Examples sourced from python-stdnum test suite
* (ar.cuit module).
*/
const cuit: Validator = {
const cuit: CountryValidator<"AR"> = {
name: "Argentine Tax ID",
localName: "Clave Única de Identificación Tributaria",
abbreviation: "CUIT",
Expand All @@ -130,6 +133,7 @@ const cuit: Validator = {
"Clave Única de Identificación Tributaria",
] as const,
candidatePattern: "\\d{2}-?\\d{8}-?\\d",
scope: "country",
country: "AR",
entityType: "any",
compact,
Expand Down
8 changes: 6 additions & 2 deletions src/ar/dni.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ const generate = (): string => randomDigits(8);
* @see https://en.wikipedia.org/wiki/Documento_Nacional_de_Identidad_(Argentina)
*/

import type { ValidateResult, Validator } from "../types";
import type {
ValidateResult,
CountryValidator,
} from "../types";

import { clean } from "#util/clean";
import { randomDigits } from "#util/generate";
Expand Down Expand Up @@ -53,7 +56,7 @@ const format = (value: string): string => {
};

/** Argentine National Identity Document. */
const dni: Validator = {
const dni: CountryValidator<"AR"> = {
name: "Argentine Identity Card",
localName: "Documento Nacional de Identidad",
abbreviation: "DNI",
Expand All @@ -63,6 +66,7 @@ const dni: Validator = {
"Documento de Identidad",
] as const,
candidatePattern: "\\d{1,2}\\.?\\d{3}\\.?\\d{3}",
scope: "country",
country: "AR",
entityType: "person",
sourceUrl:
Expand Down
8 changes: 6 additions & 2 deletions src/at/businessid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ const generate = (): string => {
* @see https://www.justiz.gv.at/
*/

import type { ValidateResult, Validator } from "../types";
import type {
ValidateResult,
CountryValidator,
} from "../types";

import { clean } from "#util/clean";
import { randomDigits, randomInt } from "#util/generate";
Expand Down Expand Up @@ -54,12 +57,13 @@ const format = (value: string): string =>
`FN ${compact(value)}`;

/** Austrian Company Register Number. */
const businessid: Validator = {
const businessid: CountryValidator<"AT"> = {
name: "Austrian Company Register Number",
localName: "Firmenbuchnummer",
abbreviation: "FN",
aliases: ["Firmenbuchnummer", "FN", "FBN"] as const,
candidatePattern: "FN\\s?\\d{5,6}[a-z]?",
scope: "country",
country: "AT",
entityType: "company",
sourceUrl: "https://www.justiz.gv.at/",
Expand Down
8 changes: 6 additions & 2 deletions src/at/tin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
* @see https://service.bmf.gv.at/Service/Anwend/Behoerden/show_mast.asp
*/

import type { ValidateResult, Validator } from "../types";
import type {
ValidateResult,
CountryValidator,
} from "../types";

import { clean } from "#util/clean";
import { randomDigits } from "#util/generate";
Expand Down Expand Up @@ -69,12 +72,13 @@ const generate = (): string => {
};

/** Austrian Tax Identification Number. */
const tin: Validator = {
const tin: CountryValidator<"AT"> = {
name: "Austrian Tax Identification Number",
localName: "Abgabenkontonummer",
abbreviation: "TIN",
aliases: ["Steuernummer", "St.Nr.", "TIN"] as const,
candidatePattern: "\\d{2}-?\\d{3}/\\d{4}",
scope: "country",
country: "AT",
entityType: "any",
sourceUrl:
Expand Down
8 changes: 6 additions & 2 deletions src/at/uid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
* @see https://www.bmf.gv.at/
*/

import type { ValidateResult, Validator } from "../types";
import type {
ValidateResult,
CountryValidator,
} from "../types";

import { luhnChecksum } from "#checksums/luhn";
import { clean } from "#util/clean";
Expand Down Expand Up @@ -68,7 +71,7 @@ const generate = (): string => {
};

/** Austrian VAT Identification Number. */
const uid: Validator = {
const uid: CountryValidator<"AT"> = {
name: "Austrian VAT Number",
localName: "Umsatzsteuer-Identifikationsnummer",
abbreviation: "UID",
Expand All @@ -78,6 +81,7 @@ const uid: Validator = {
"ATU",
] as const,
candidatePattern: "ATU\\d{8}",
scope: "country",
country: "AT",
entityType: "company",
sourceUrl: "https://www.bmf.gv.at/",
Expand Down
Loading
Loading