{"_id":"@alosha/eu-validate","_rev":"4-bdf80afd65ffe8a682db8f8cefdbea38","name":"@alosha/eu-validate","dist-tags":{"latest":"0.5.0"},"versions":{"0.1.0":{"name":"@alosha/eu-validate","version":"0.1.0","keywords":["vat","vat-validation","vies","iban","bsn","kvk","eu","european-union","validation","checksum","compliance","e-invoicing","typescript","zero-dependency"],"author":{"name":"Eduardo Silvanavarrete","email":"avlis_odraude@hotmail.com"},"license":"MIT","_id":"@alosha/eu-validate@0.1.0","maintainers":[{"name":"avlisodraude","email":"avlis_odraude@hotmail.com"}],"homepage":"https://eu-validate.alosha.dev","bugs":{"url":"https://github.com/avlisodraude/eu-validate/issues"},"dist":{"shasum":"f0cee5e6421799b335cc162cd537062d12eb9cbe","tarball":"https://registry.npmjs.org/@alosha/eu-validate/-/eu-validate-0.1.0.tgz","fileCount":15,"integrity":"sha512-xIcIjMko/66nXf1IA696voEP3g3H3kwt6t72jBKLfdGBRU7c5qpQF6uaOSFkv8HU9vllOljBLcIkz+MLgeVBfw==","signatures":[{"sig":"MEQCIGlgL31ZBV/NX8YBGS/qhopCbGWE2d8Wq5mIOHtWISQLAiAtCfk9Iz/Z/QG5ucYcmk37F/G+OCcaQJUxYvZkMgXt3A==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":109265},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./cloud":{"import":{"types":"./dist/cloud.d.ts","default":"./dist/cloud.js"},"require":{"types":"./dist/cloud.d.cts","default":"./dist/cloud.cjs"}}},"gitHead":"c67cfc68dcad7c3dbc3b9f431c31a19defa9154f","scripts":{"dev":"tsup --watch","lint":"eslint src","test":"node --experimental-vm-modules node_modules/.bin/jest","build":"tsup","prepare":"husky","release":"npm run build && npm publish --access public"},"_npmUser":{"name":"avlisodraude","email":"avlis_odraude@hotmail.com"},"repository":{"url":"git+https://github.com/avlisodraude/eu-validate.git","type":"git"},"_npmVersion":"11.16.0","description":"Validate EU VAT, IBAN, and Dutch BSN/KvK numbers — offline, zero-dependency, fully typed. Add live VIES + KvK lookups with one API key.","directories":{},"_nodeVersion":"22.22.3","dependencies":{},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","tsup":"^8.1.0","husky":"^9.0.11","eslint":"^9.5.0","ts-jest":"^29.2.0","@eslint/js":"^9.5.0","typescript":"^5.4.5","@types/jest":"^29.5.12","@types/node":"^20.14.0","lint-staged":"^15.2.7","@commitlint/cli":"^19.3.0","typescript-eslint":"^8.0.0","@commitlint/config-conventional":"^19.2.2"},"_npmOperationalInternal":{"tmp":"tmp/eu-validate_0.1.0_1781520951908_0.28362067635891375","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@alosha/eu-validate","version":"0.3.0","keywords":["vat","vat-validation","vies","iban","bsn","kvk","eu","european-union","validation","checksum","compliance","e-invoicing","typescript","zero-dependency"],"author":{"name":"Eduardo Silvanavarrete","email":"avlis_odraude@hotmail.com"},"license":"MIT","_id":"@alosha/eu-validate@0.3.0","maintainers":[{"name":"avlisodraude","email":"avlis_odraude@hotmail.com"}],"homepage":"https://eu-validate.alosha.dev","bugs":{"url":"https://github.com/avlisodraude/eu-validate/issues"},"dist":{"shasum":"922a209c35aba026c98682b75a2cb004b041ecaf","tarball":"https://registry.npmjs.org/@alosha/eu-validate/-/eu-validate-0.3.0.tgz","fileCount":15,"integrity":"sha512-CqYkR515d+lgHpSrHHlEFnM02SBqNxENKv87i6ER/afdyHLGEubvUrqIkdzDiExEIqKvq2zLxOBRKsRG0Fzkbg==","signatures":[{"sig":"MEYCIQCWOx3ZsNFKtv7Qh1jbhKiLRn5XZKEgpL8mUftN1FEIggIhAPsdkyJmfi9nRBT2R4O2TjTWoAOZFxdEQ5K+ny2uONYD","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":137994},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./cloud":{"import":{"types":"./dist/cloud.d.ts","default":"./dist/cloud.js"},"require":{"types":"./dist/cloud.d.cts","default":"./dist/cloud.cjs"}}},"gitHead":"a611b80c96c665a71f8152fc2ad1ecb5eb0ffed0","scripts":{"dev":"tsup --watch","lint":"eslint src","test":"node --experimental-vm-modules node_modules/.bin/jest","build":"tsup","prepare":"husky","release":"npm run build && npm publish --access public"},"_npmUser":{"name":"avlisodraude","email":"avlis_odraude@hotmail.com"},"overrides":{"esbuild":">=0.28.1","js-yaml":"^4.2.0"},"repository":{"url":"git+https://github.com/avlisodraude/eu-validate.git","type":"git"},"_npmVersion":"11.16.0","description":"Validate EU VAT, IBAN, and Dutch BSN/KvK numbers — offline, zero-dependency, fully typed. Add live VIES + KvK lookups with one API key.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.3","_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","tsup":"^8.1.0","husky":"^9.0.11","eslint":"^9.5.0","ts-jest":"^29.2.0","@eslint/js":"^9.5.0","typescript":"^5.4.5","@types/jest":"^29.5.12","@types/node":"^20.14.0","lint-staged":"^15.2.7","@commitlint/cli":"^19.3.0","typescript-eslint":"^8.61.1","@commitlint/config-conventional":"^19.2.2"},"_npmOperationalInternal":{"tmp":"tmp/eu-validate_0.3.0_1782810708563_0.6250407977550296","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@alosha/eu-validate","version":"0.4.0","keywords":["vat","vat-validation","vies","iban","bsn","kvk","eu","european-union","validation","checksum","compliance","e-invoicing","typescript","zero-dependency"],"author":{"name":"Eduardo Silvanavarrete","email":"avlis_odraude@hotmail.com"},"license":"MIT","_id":"@alosha/eu-validate@0.4.0","maintainers":[{"name":"avlisodraude","email":"avlis_odraude@hotmail.com"}],"homepage":"https://eu-validate.alosha.dev","bugs":{"url":"https://github.com/avlisodraude/eu-validate/issues"},"dist":{"shasum":"2a8197bda1b1dfd66c6f2bc77c2916fcaf2bfa67","tarball":"https://registry.npmjs.org/@alosha/eu-validate/-/eu-validate-0.4.0.tgz","fileCount":15,"integrity":"sha512-+eSebSrVYWv1ixZZpZqkLTmTgY2Ji1OBqcRB2Zmr1vPPsGnIL5eyF7FM8Aonkdh1IWvqzP/xmoVgJUME5mzkDw==","signatures":[{"sig":"MEUCIBY8967cT25Lti+EueMx0EiF/4KTNMjSINIu9NX352jCAiEAqC60QmOf23Ehh89cP08R6ji85IWM5bAnqqjP33mS1jA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":173370},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./cloud":{"import":{"types":"./dist/cloud.d.ts","default":"./dist/cloud.js"},"require":{"types":"./dist/cloud.d.cts","default":"./dist/cloud.cjs"}}},"gitHead":"c86db360c4c8a2fbf76a3c3626ed13cb04e25424","scripts":{"dev":"tsup --watch","lint":"eslint src","test":"node --experimental-vm-modules node_modules/.bin/jest","build":"tsup","prepare":"husky","release":"npm run build && npm publish --access public"},"_npmUser":{"name":"avlisodraude","email":"avlis_odraude@hotmail.com"},"overrides":{"esbuild":">=0.28.1","js-yaml":"^4.2.0"},"repository":{"url":"git+https://github.com/avlisodraude/eu-validate.git","type":"git"},"_npmVersion":"11.16.0","description":"Validate EU VAT, IBAN, and Dutch BSN/KvK numbers — offline, zero-dependency, fully typed. VAT checksums for 22 of 27 EU countries. Live VIES + KvK lookups coming soon.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.3","_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","tsup":"^8.1.0","husky":"^9.0.11","eslint":"^9.5.0","ts-jest":"^29.2.0","@eslint/js":"^9.5.0","typescript":"^5.4.5","@types/jest":"^29.5.12","@types/node":"^20.14.0","lint-staged":"^15.2.7","@commitlint/cli":"^19.3.0","typescript-eslint":"^8.61.1","@commitlint/config-conventional":"^19.2.2"},"_npmOperationalInternal":{"tmp":"tmp/eu-validate_0.4.0_1783578148327_0.08729191703335681","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@alosha/eu-validate","version":"0.5.0","description":"Validate EU VAT, IBAN, and Dutch BSN/KvK numbers — offline, zero-dependency, fully typed. VAT checksums for all 27 EU countries. Live VIES + KvK lookups coming soon.","keywords":["vat","vat-validation","vies","iban","bsn","kvk","eu","european-union","validation","checksum","compliance","e-invoicing","typescript","zero-dependency"],"homepage":"https://eu-validate.alosha.dev","bugs":{"url":"https://github.com/avlisodraude/eu-validate/issues"},"repository":{"type":"git","url":"git+https://github.com/avlisodraude/eu-validate.git"},"license":"MIT","author":{"name":"Eduardo Silva Navarrete","email":"avlis_odraude@hotmail.com"},"type":"module","sideEffects":false,"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./cloud":{"import":{"types":"./dist/cloud.d.ts","default":"./dist/cloud.js"},"require":{"types":"./dist/cloud.d.cts","default":"./dist/cloud.cjs"}}},"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","scripts":{"build":"tsup","dev":"tsup --watch","test":"node --experimental-vm-modules node_modules/.bin/jest","lint":"eslint src","prepare":"husky","release":"npm run build && npm publish --access public"},"devDependencies":{"@commitlint/cli":"^19.3.0","@commitlint/config-conventional":"^19.2.2","@eslint/js":"^9.5.0","@types/jest":"^29.5.12","@types/node":"^20.14.0","eslint":"^9.5.0","husky":"^9.0.11","jest":"^29.7.0","lint-staged":"^15.2.7","ts-jest":"^29.2.0","tsup":"^8.1.0","typescript":"^5.4.5","typescript-eslint":"^8.61.1"},"overrides":{"esbuild":">=0.28.1","js-yaml":"^4.2.0"},"gitHead":"685d7558e1bd36f75a8bf142759af0b90c50031a","_id":"@alosha/eu-validate@0.5.0","_nodeVersion":"22.22.3","_npmVersion":"11.16.0","dist":{"integrity":"sha512-NhMNsSr5f38klB736sqnGeUtKb6Nw7385yMho5sUY1CQl3G41hgffkpkemUpfToGqsGyT2jfmPrqzeRd8PiGLQ==","shasum":"b0b31123862f3a9771474b6a2d3145821f60bfc9","tarball":"https://registry.npmjs.org/@alosha/eu-validate/-/eu-validate-0.5.0.tgz","fileCount":15,"unpackedSize":190827,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIH5WeJORCY07pVJmhOgwiKLm5cLMW98fJWcCKEEBfdMlAiEAmpkwS83oEO7FE9fmeXSxsYJ40wTAOJjp7tG3r5QeEHo="}]},"_npmUser":{"name":"avlisodraude","email":"avlis_odraude@hotmail.com"},"directories":{},"maintainers":[{"name":"avlisodraude","email":"avlis_odraude@hotmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/eu-validate_0.5.0_1783617161894_0.3398177073570985"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-15T10:55:51.679Z","modified":"2026-07-09T17:12:42.137Z","0.1.0":"2026-06-15T10:55:52.041Z","0.3.0":"2026-06-30T09:11:48.705Z","0.4.0":"2026-07-09T06:22:28.487Z","0.5.0":"2026-07-09T17:12:42.023Z"},"bugs":{"url":"https://github.com/avlisodraude/eu-validate/issues"},"author":{"name":"Eduardo Silva Navarrete","email":"avlis_odraude@hotmail.com"},"license":"MIT","homepage":"https://eu-validate.alosha.dev","keywords":["vat","vat-validation","vies","iban","bsn","kvk","eu","european-union","validation","checksum","compliance","e-invoicing","typescript","zero-dependency"],"repository":{"type":"git","url":"git+https://github.com/avlisodraude/eu-validate.git"},"description":"Validate EU VAT, IBAN, and Dutch BSN/KvK numbers — offline, zero-dependency, fully typed. VAT checksums for all 27 EU countries. Live VIES + KvK lookups coming soon.","maintainers":[{"name":"avlisodraude","email":"avlis_odraude@hotmail.com"}],"readme":"# @alosha/eu-validate\n\nValidate EU **VAT**, **IBAN**, and Dutch **BSN/KvK** numbers — offline, zero-dependency, fully typed. VAT checksums for all 27 EU countries. Live **VIES + KvK** lookups coming soon.\n\n[![npm version](https://img.shields.io/npm/v/@alosha/eu-validate)](https://www.npmjs.com/package/@alosha/eu-validate)\n[![npm downloads](https://img.shields.io/npm/dm/@alosha/eu-validate)](https://www.npmjs.com/package/@alosha/eu-validate)\n[![Gzip size](https://img.shields.io/bundlephobia/minzip/@alosha/eu-validate)](https://unpkg.com/@alosha/eu-validate/dist/index.js)\n[![Types included](https://img.shields.io/badge/types-included-blue?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n**▶ [Try the live demo](https://eu-validate.alosha.dev/demo)** — validate VAT, IBAN, BSN, KvK and postal codes right in your browser. No install, nothing uploaded.\n\n- ✅ **Zero runtime dependencies** — pure, deterministic checks. Nothing is fetched.\n- 🧮 **Real checksums**, not just regex — VAT for all 27 EU countries, IBAN (ISO 13616 mod-97), Dutch BSN (11-proof).\n- 🌍 **Format validation for all 27 EU VAT formats** + SEPA IBAN lengths.\n- 🔠 **One consistent result shape** across every identifier type.\n- 🧩 **Tree-shakeable** ESM + CJS, TypeScript types included.\n- ☁️ **Cloud client (coming soon)** for VIES registration checks and KvK company lookups — the hosted API is not yet available.\n\n## Install\n\n```bash\nnpm i @alosha/eu-validate\n```\n\n## Usage\n\n```ts\nimport { validateVAT, validateIBAN, validateBSN } from '@alosha/eu-validate'\n\nvalidateVAT('NL123456782B01')\n// { valid: true, normalized: 'NL123456782B01', country: 'NL', type: 'vat',\n//   checks: { format: true, checksum: true }, errors: [] }\n\nvalidateIBAN('NL91 ABNA 0417 1643 00')\n// { valid: true, normalized: 'NL91ABNA0417164300', country: 'NL', ... }\n\nvalidateBSN('111222333')\n// { valid: true, ... }\n```\n\nInput is cleaned automatically (spaces, dots, dashes, casing), so `nl 1234.567.82 b01` works too.\n\n### The result shape\n\nEvery validator returns the same object:\n\n```ts\ninterface ValidationResult {\n  valid: boolean              // offline format + checksum validity\n  input: string               // your original input\n  normalized: string | null   // canonical form\n  country: string | null      // ISO alpha-2\n  type: 'vat' | 'iban' | 'bsn' | 'kvk' | 'postalCode'\n  checks: { format: boolean; checksum: boolean | null }  // null = no checksum for this case\n  errors: string[]            // 'INVALID_FORMAT' | 'CHECKSUM_FAILED' | ...\n}\n```\n\n### Dispatcher\n\nWhen the type is known at runtime (e.g. a form field):\n\n```ts\nimport { validate } from '@alosha/eu-validate'\n\nvalidate('1011 AB', { type: 'postalCode', country: 'NL' })\n```\n\n### Convenience helpers\n\nFor call sites that want to branch or throw instead of checking `.valid` by hand:\n\n```ts\nimport { validateIBAN, isValid, assertValid, ValidationError } from '@alosha/eu-validate'\n\nconst r = validateIBAN('NL91ABNA0417164300')\n\nif (isValid(r)) {\n  // r narrowed to valid: true\n}\n\ntry {\n  const checked = assertValid(validateIBAN(input)) // throws ValidationError if invalid\n} catch (e) {\n  if (e instanceof ValidationError) {\n    console.log(e.result.errors) // the failing ValidationResult\n  }\n}\n```\n\n## Production recipes\n\nReal problems, complete solutions — copy, paste, ship.\n\n### Reject bad VAT numbers before you ever call VIES\n\n**The problem:** a B2B checkout must apply reverse-charge VAT, but VIES is slow, rate-limited, and rejects malformed input anyway.\n\n```ts\nimport { validateVAT } from '@alosha/eu-validate'\nimport { createClient } from '@alosha/eu-validate/cloud'\n\nconst eu = createClient({ apiKey: process.env.ALOSHA_KEY! })\n\nexport async function resolveVat(input: string) {\n  // 1. Offline first — structure + checksum, zero network, instant.\n  const offline = validateVAT(input)\n  if (!offline.valid) {\n    return { ok: false, reason: offline.errors[0] } // e.g. 'CHECKSUM_FAILED'\n  }\n\n  // 2. Spend a VIES round-trip only on numbers that already pass the checksum.\n  const live = await eu.verifyVAT(offline.normalized!)\n  return { ok: live.registered, company: live.name }\n}\n```\n\n**Why it works:** the offline checksum filters out typos and fabricated numbers for free, so the slow, rate-limited VIES call only ever runs on structurally valid input. You cut checkout latency and stop burning your VIES quota on garbage.\n\n### Validate Dutch BSN and IBAN without sending PII anywhere\n\n**The problem:** an onboarding form collects a BSN and IBAN, but shipping those to a third-party validation API is a GDPR data-egress problem.\n\n```ts\nimport { validateBSN, validateIBAN } from '@alosha/eu-validate'\n\n// Pure, synchronous, offline — the values never leave the user's session.\nexport function validateOnboarding(form: { bsn: string; iban: string }) {\n  const bsn = validateBSN(form.bsn)\n  const iban = validateIBAN(form.iban)\n\n  return {\n    valid: bsn.valid && iban.valid,\n    fields: {\n      bsn: bsn.valid ? null : bsn.errors[0],   // e.g. 'CHECKSUM_FAILED'\n      iban: iban.valid ? null : iban.errors[0] // e.g. 'INVALID_FORMAT'\n    }\n  }\n}\n```\n\n**Why it works:** every validator is a pure function with no network call, so sensitive identifiers like a BSN never reach an external processor. You get instant inline form feedback and one fewer data-processing agreement to sign.\n\n### Confirm VAT registration when you can, fall back gracefully when you can't\n\n**The problem:** a live VIES registration check on top of the offline checksum can fail for reasons that have nothing to do with the VAT number — the hosted endpoint isn't live yet, a timeout, a bad response — and none of those should look like \"this VAT is invalid.\"\n\n```ts\nimport { validateVAT } from '@alosha/eu-validate'\nimport { createClient, CloudNotAvailableError, CloudTimeoutError, CloudApiError } from '@alosha/eu-validate/cloud'\n\nconst eu = createClient({ apiKey: process.env.ALOSHA_KEY! })\n\nexport async function checkVat(input: string) {\n  const offline = validateVAT(input)\n  if (!offline.valid) {\n    return { status: 'invalid' as const, reason: offline.errors[0] }\n  }\n\n  try {\n    const live = await eu.verifyVAT(offline.normalized!)\n    return { status: (live.registered ? 'registered' : 'not_registered') as const, company: live.name }\n  } catch (err) {\n    if (err instanceof CloudNotAvailableError) {\n      // Hosted lookups aren't live yet — the offline checksum already passed, so degrade\n      // instead of failing the request.\n      return { status: 'format_valid_unconfirmed' as const, reason: 'cloud_not_available' }\n    }\n    if (err instanceof CloudTimeoutError || err instanceof CloudApiError) {\n      // Transient — don't tell the user their VAT number is wrong because VIES hiccuped.\n      return { status: 'format_valid_unconfirmed' as const, reason: 'cloud_error' }\n    }\n    throw err\n  }\n}\n```\n\n**Why it works:** `verifyVAT()` throws typed errors instead of a generic `Error`, so a Cloud outage or a not-yet-deployed hosted endpoint never gets confused with \"the VAT number is wrong.\" The offline checksum already did the hard rejection work, so every Cloud failure mode here degrades to \"unconfirmed\" instead of blocking the user.\n\n### Reject malformed identifiers at the edge of your API\n\n**The problem:** every route that accepts a VAT, IBAN or BSN re-implements the same `if (!result.valid) return res.status(400)...` boilerplate, and it's easy for one route to forget a field.\n\n```ts\nimport express from 'express'\nimport { validateIBAN, validateVAT, assertValid, ValidationError } from '@alosha/eu-validate'\n\nconst app = express()\napp.use(express.json())\n\napp.post('/payouts', (req, res) => {\n  try {\n    const iban = assertValid(validateIBAN(req.body.iban))\n    const vat = assertValid(validateVAT(req.body.vat))\n    return res.json({ ok: true, iban: iban.normalized, vat: vat.normalized })\n  } catch (err) {\n    if (err instanceof ValidationError) {\n      return res.status(400).json({ ok: false, type: err.result.type, errors: err.result.errors })\n    }\n    throw err\n  }\n})\n```\n\n**Why it works:** `assertValid()` turns the usual \"check `.valid`, then branch\" dance into a single throw, so one `catch` block at the route boundary handles every identifier field the same way. `ValidationError` carries the full failing `ValidationResult`, so the 400 response tells the caller exactly which field and error code failed — no per-route boilerplate.\n\n### Validate a full Dutch company-onboarding form in one pass\n\n**The problem:** a B2B signup form for the Netherlands collects four different identifier types — KvK, BSN, IBAN, VAT — and hand-wiring four separate `validateX()` calls means the field list and the validator list drift apart as the form grows.\n\n```ts\nimport { validate, type ValidateOptions } from '@alosha/eu-validate'\n\nconst ONBOARDING_FIELDS: Record<string, ValidateOptions> = {\n  kvkNumber: { type: 'kvk' },\n  bsn: { type: 'bsn' },\n  iban: { type: 'iban' },\n  vatNumber: { type: 'vat' }\n}\n\nexport function validateOnboardingForm(form: Record<string, string>) {\n  const fields = Object.fromEntries(\n    Object.entries(ONBOARDING_FIELDS).map(([field, options]) => [\n      field,\n      validate(form[field] ?? '', options)\n    ])\n  )\n\n  return {\n    valid: Object.values(fields).every((r) => r.valid),\n    fields // each entry is a full ValidationResult — keep `errors` for inline form feedback\n  }\n}\n```\n\n**Why it works:** the dispatcher means the field list is the single source of truth — add a row to `ONBOARDING_FIELDS` and the loop picks it up, instead of a fifth hand-written `validateX()` call drifting out of sync with the form. Every field still gets the same typed `ValidationResult`, so existing per-field error rendering keeps working unchanged.\n\n### Clean up a customer VAT list before a VIES batch run\n\n**The problem:** a finance team exports thousands of customer VAT numbers for a quarterly VIES re-verification, and running every row through VIES — typos, copy-paste artifacts and all — wastes the rate-limited quota on input that was never going to pass.\n\n```ts\nimport { readFileSync, writeFileSync } from 'node:fs'\nimport { validateVAT } from '@alosha/eu-validate'\n\nconst rows = readFileSync('customers.csv', 'utf8')\n  .trim()\n  .split('\\n')\n  .slice(1) // drop header\n  .map((line) => line.split(','))\n\nconst clean: string[] = ['customer_id,vat_number']\nconst rejected: string[] = ['customer_id,vat_number,error']\n\nfor (const [customerId, vat] of rows) {\n  const result = validateVAT(vat)\n  if (result.valid) {\n    clean.push(`${customerId},${result.normalized}`)\n  } else {\n    rejected.push(`${customerId},${vat},${result.errors[0]}`)\n  }\n}\n\nwriteFileSync('clean.csv', clean.join('\\n'))\nwriteFileSync('rejected.csv', rejected.join('\\n'))\nconsole.log(`${clean.length - 1} clean, ${rejected.length - 1} rejected — only clean.csv needs a VIES call.`)\n```\n\n**Why it works:** the checksum pass is synchronous and free, so a list of 10,000 numbers is sorted into \"worth a VIES call\" and \"already known bad\" in milliseconds, with the specific error code attached to every rejected row for the finance team to act on. You spend VIES quota only on numbers that have a chance of being real.\n\n## What \"valid\" means\n\nThis library answers **\"is this well-formed?\"** — it never makes a network request, so it cannot tell you whether a number is *registered* or belongs to a real company. For that, use the Cloud client.\n\n| | Offline (this library) | Cloud (`/cloud`) |\n|---|---|---|\n| VAT | format + checksum | VIES lookup — registered & active, with name/address |\n| KvK | format (8 digits) | KvK register — company name, status, address |\n| IBAN | mod-97 + length | — (offline is enough) |\n| BSN | 11-proof | — (no public lookup) |\n\n```ts\nimport { createClient } from '@alosha/eu-validate/cloud'\n\nconst eu = createClient({ apiKey: process.env.ALOSHA_KEY! })\nawait eu.verifyVAT('NL123456782B01') // → VIES result\nawait eu.lookupKvK('69599084')       // → KvK company data\n```\n\n> The Cloud API is rolling out. The client surface is stable today, and requests hit the real hosted endpoint — no package upgrade needed once it's live for your account.\n\nIf the hosted endpoint isn't deployed yet (404, or the connection fails at the DNS/socket level), `verifyVAT()` / `lookupKvK()` throw a typed `CloudNotAvailableError` (rather than a generic `Error`) so you can catch it specifically. Once live, the client throws `CloudTimeoutError` (request exceeded `timeoutMs`) or `CloudApiError` (non-2xx response, with `status`/`statusText`/`body`) — all three are exported from `@alosha/eu-validate/cloud`.\n\n## Coverage (V1)\n\n- **VAT checksum:** all 27 EU countries. `checks.checksum` is `null` only for\n  per-number sub-cases with no offline-checkable formula (FR alphabetic keys,\n  CZ pre-1954 birth numbers, LV natural persons).\n- **IBAN:** all SEPA / IBAN-registry countries\n- **BSN / KvK:** 🇳🇱 NL\n- **Postal codes:** NL, DE, FR, BE, ES, IT\n\n### Note on NL VAT\n\nThe NL checksum validates both number styles offline. Company (legal-entity) numbers pass the 11-proof on the first 9 digits. Sole-trader BTW-id numbers issued since 2020 are randomized and deliberately fail the 11-proof, but instead satisfy a mod-97 check over the full `NL`-prefixed string (letters converted to digits, A=10..Z=35). `checkNL` accepts a number if *either* check passes.\n\n### Note on Greece: GR vs EL\n\nGreece's ISO 3166-1 country code is `GR`, but its VAT prefix is `EL`. `validateVAT()` accepts a `GR...` input and normalizes it to `EL...` — so a Greek VAT result's `country` field is `'EL'`. `validateIBAN()` (and `validatePostalCode()`) instead use the ISO code, so a Greek IBAN/postal result's `country` field is `'GR'`. This isn't a bug — VIES itself uses `EL` — but if you're cross-referencing a VAT result against an IBAN result for the same Greek business, compare on `'EL' === 'GR' ? ...` logic rather than assuming the `country` fields match.\n\n### Note on FR VAT and `CHECKSUM_NOT_VERIFIABLE`\n\nFrench VAT numbers can have either a numeric key (formula-checkable) or an alphabetic key (not formula-checkable offline). For the alphabetic-key case, `validateVAT()` returns `valid: true` with `checks.checksum: null` and `errors: ['CHECKSUM_NOT_VERIFIABLE']` — the format is confirmed correct, but the checksum itself couldn't be confirmed. This is the one case where `errors` is non-empty on a valid result; check `checks.checksum === null` if you want to distinguish \"checksum verified\" from \"checksum not checkable\" programmatically.\n\n## Develop\n\n```bash\nnpm install\nnpm run build   # tsup → ESM + CJS + d.ts\nnpm test        # jest\nnpm run lint\n```\n\n## Support & custom work\n\n`@alosha/eu-validate` is free and MIT-licensed, and always will be. When you need more than offline validation, there's a paid path backed by the maintainer — not a ticket queue:\n\n- **Cloud lookups** — hosted VIES VAT registration and KvK company lookups via `@alosha/eu-validate/cloud` (coming soon).\n- **Priority support** — a direct line to the person who wrote it, with prioritised fixes.\n- **Custom work** — extra country coverage or custom validators on request.\n\nGet in touch at [alosha.dev/support](https://alosha.dev/support).\n\n## License\n\nMIT © Eduardo Silva Navarrete\n\n---\n\nDocs & live demo: [eu-validate.alosha.dev](https://eu-validate.alosha.dev) · Built by [Alosha](https://alosha.dev)\n","readmeFilename":"README.md"}