aamva-decoder 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,254 @@
1
+ # aamva-decoder
2
+
3
+ [![npm version](https://img.shields.io/npm/v/aamva-decoder)](https://www.npmjs.com/package/aamva-decoder)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/types-TypeScript-blue.svg)](https://www.typescriptlang.org/)
6
+ [![Node.js](https://img.shields.io/badge/node-18%20%7C%2020%20%7C%2022-green.svg)](https://nodejs.org/)
7
+
8
+ Parse PDF-417 barcode data from US and Canadian driver's licenses. Supports AAMVA versions 1 through 11 (2025 standard).
9
+
10
+ ## Usage
11
+
12
+ ```ts
13
+ import { parseLicense } from "aamva-decoder";
14
+
15
+ const barcodeData: string = /* scan PDF-417 barcode */;
16
+ const license = parseLicense(barcodeData);
17
+
18
+ console.log(license.firstName); // "JOHN"
19
+ console.log(license.lastName); // "PUBLIC"
20
+ console.log(license.dateOfBirth); // Date object
21
+ console.log(license.state); // "CA"
22
+ console.log(license.aamvaVersion); // 11
23
+ ```
24
+
25
+ If you already have a parsed `BarcodeFile` (from `parseBarcodeString`), you can skip re-parsing:
26
+
27
+ ```ts
28
+ import { parseBarcodeString, parseLicenseFromBarcode } from "aamva-decoder";
29
+
30
+ const barcode = parseBarcodeString(barcodeData);
31
+ const license = parseLicenseFromBarcode(barcode);
32
+ ```
33
+
34
+ ### Raw Element Access
35
+
36
+ Every `License` object includes a `raw` field containing all element key-value pairs from the barcode. This is useful for accessing jurisdiction-specific or future element IDs that aren't mapped to named fields:
37
+
38
+ ```ts
39
+ const license = parseLicense(barcodeData);
40
+ console.log(license.raw["ZVA"]); // jurisdiction-specific element
41
+ console.log(license.raw["DCA"]); // raw vehicle class code
42
+ ```
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ npm install aamva-decoder
48
+ ```
49
+
50
+ ## AAMVA Standard
51
+
52
+ ### Supported Fields
53
+
54
+ | Name | Description | Element ID | Type | `License` Attribute |
55
+ |:-----|:------------|:----------:|:-----|:--------------------|
56
+ | First Name | Customer first name | DAC | `string` | `firstName` |
57
+ | Last Name | Customer last name | DCS | `string` | `lastName` |
58
+ | Middle Name | Customer middle name | DAD | `string` | `middleName` |
59
+ | Name Suffix | Name suffix (JR, SR, etc.) | DCU | `string` | `nameSuffix` |
60
+ | Name Prefix | Name prefix (DR, etc.) | DAF | `string` | `namePrefix` |
61
+ | First Name (v2-3) | First name element used in v2-3 | DCT | `string` | `firstNameV2` |
62
+ | Date of Birth | Customer date of birth | DBB | `Date` | `dateOfBirth` |
63
+ | Expiration Date | Document expiration date | DBA | `Date` | `expirationDate` |
64
+ | Issue Date | Document issue date | DBD | `Date` | `issueDate` |
65
+ | Sex | Customer sex | DBC | `string` | `sex` |
66
+ | Eye Color | Customer eye color | DAY | `string` | `eyeColor` |
67
+ | Hair Color | Customer hair color | DAZ | `string` | `hairColor` |
68
+ | Height | Customer height | DAU | `string` | `height` |
69
+ | Height (cm) | Height in centimeters | DAV | `string` | `heightCm` |
70
+ | Weight | Customer weight (lbs) | DAW | `string` | `weight` |
71
+ | Weight (kg) | Weight in kilograms | DAX | `string` | `weightKg` |
72
+ | Weight Range | Weight range code (0-9) | DCE | `string` | `weightRange` |
73
+ | Street Address | Customer street address | DAG | `string` | `streetAddress` |
74
+ | Street Address 2 | Street address line 2 | DAH | `string` | `streetAddress2` |
75
+ | City | Customer city | DAI | `string` | `city` |
76
+ | State | Customer state | DAJ | `string` | `state` |
77
+ | Postal Code | Customer postal code | DAK | `string` | `postalCode` |
78
+ | Country | Issuing country | DCG | `string` | `country` |
79
+ | Document Number | Unique customer ID number | DAQ/DBJ | `string` | `documentNumber` |
80
+ | Document Discriminator | Unique document ID number | DCF | `string` | `documentDiscriminator` |
81
+ | Vehicle Class | Jurisdiction-specific vehicle class | DCA/DAR | `string` | `vehicleClass` |
82
+ | Restriction Codes | Jurisdiction-specific restriction codes | DCB/DAS | `string` | `restrictionCodes` |
83
+ | Endorsement Codes | Jurisdiction-specific endorsement codes | DCD/DAT | `string` | `endorsementCodes` |
84
+ | Federal Commercial Vehicle Codes | Federal commercial vehicle codes | DCH | `string` | `federalCommercialVehicleCodes` |
85
+ | Standard Vehicle Classification | Standard vehicle classification | DCM | `string` | `standardVehicleClassification` |
86
+ | Standard Endorsement Code | Standard endorsement code | DCN | `string` | `standardEndorsementCode` |
87
+ | Standard Restriction Code | Standard restriction code | DCO | `string` | `standardRestrictionCode` |
88
+ | Vehicle Class Description | Text description of vehicle class | DCP | `string` | `vehicleClassDescription` |
89
+ | Endorsement Code Description | Text description of endorsements | DCQ | `string` | `endorsementCodeDescription` |
90
+ | Restriction Code Description | Text description of restrictions | DCR | `string` | `restrictionCodeDescription` |
91
+ | Compliance Type | REAL ID compliance ("F"=compliant, "N"=non-compliant) | DDA | `string` | `complianceType` |
92
+ | Card Revision Date | Most recent card revision date | DDB | `Date` | `cardRevisionDate` |
93
+ | Limited Duration Document | Is this a limited duration document? | DDD | `boolean` | `limitedDurationDocument` |
94
+ | Organ Donor | Is the cardholder an organ donor? | DDK/DBH | `boolean` | `organDonor` |
95
+ | Organ Donor (legacy) | Organ donor indicator (older format, pre-DDK) | DBH | `string` | `organDonorLegacy` |
96
+ | Veteran | Is the cardholder a veteran? | DDL | `boolean` | `veteran` |
97
+ | First Name Truncation | Was first name truncated? ("T"/"N") | DDF | `string` | `firstNameTruncation` |
98
+ | Middle Name Truncation | Was middle name truncated? ("T"/"N") | DDG | `string` | `middleNameTruncation` |
99
+ | Last Name Truncation | Was last name truncated? ("T"/"N") | DDE | `string` | `lastNameTruncation` |
100
+ | Place of Birth | Country/municipality/state of birth | DCI | `string` | `placeOfBirth` |
101
+ | Audit Information | Identifies when, where, and by whom the card was made | DCJ | `string` | `auditInformation` |
102
+ | Inventory Control Number | Identifies raw materials used in card production | DCK | `string` | `inventoryControlNumber` |
103
+ | Last Name Alias | Other last name by which cardholder is known | DBN/DBO | `string` | `lastNameAlias` |
104
+ | First Name Alias | Other first name by which cardholder is known | DBG/DBP | `string` | `firstNameAlias` |
105
+ | Suffix Alias | Other suffix by which cardholder is known | DBS | `string` | `suffixAlias` |
106
+ | Race/Ethnicity | Customer race/ethnicity (v3-v10, removed in v11) | DCL | `string` | `raceEthnicity` |
107
+ | HAZMAT Endorsement Exp. | HAZMAT endorsement expiration date (v3-v10, removed in v11) | DDC | `Date` | `hazmatEndorsementExpiration` |
108
+ | CDL Indicator | Is this a commercial driver's license? (v11+) | DDM | `boolean` | `cdlIndicator` |
109
+ | Non-Domiciled Indicator | Is this a non-domiciled license? (v11+) | DDN | `boolean` | `nonDomiciledIndicator` |
110
+ | Enhanced Document Indicator | Is this an enhanced document? (v11+) | DDO | `boolean` | `enhancedDocumentIndicator` |
111
+ | Permit Indicator | Is this a permit? (v11+) | DDP | `boolean` | `permitIndicator` |
112
+ | Issue Timestamp | Date and time of card issuance | DBE | `string` | `issueTimestamp` |
113
+ | Number of Duplicates | Number of duplicate cards issued | DBF | `string` | `numberOfDuplicates` |
114
+ | Non-Resident Indicator | Non-resident indicator | DBI | `string` | `nonResidentIndicator` |
115
+ | Unique Customer ID | Unique customer identifier (v1, maps to DAQ) | DBJ | `string` | `uniqueCustomerId` |
116
+ | Social Security Number | Social security number (deprecated) | DBK | `string` | `socialSecurityNumber` |
117
+ | AKA Date of Birth | AKA date of birth | DBL | `Date` | `akaDateOfBirth` |
118
+ | AKA Social Security Number | AKA social security number (deprecated) | DBM | `string` | `akaSocialSecurityNumber` |
119
+ | Under 18 Until | Date cardholder turns 18 | DDH | `Date` | `under18Until` |
120
+ | Under 19 Until | Date cardholder turns 19 | DDI | `Date` | `under19Until` |
121
+ | Under 21 Until | Date cardholder turns 21 | DDJ | `Date` | `under21Until` |
122
+
123
+ #### V1 Legacy Fields
124
+
125
+ These fields use v1-specific element IDs. Their values are also normalized into the modern field names above as fallbacks.
126
+
127
+ | Name | Description | Element ID | `License` Attribute |
128
+ |:-----|:------------|:----------:|:--------------------|
129
+ | Last Name (v1) | Last name (v1 element ID) | DAB | `lastNameV1` |
130
+ | Name Suffix (v1) | Name suffix (v1 element ID) | DAE | `nameSuffixV1` |
131
+ | Classification Code (v1) | Vehicle classification (v1 element ID) | DAR | `classificationCodeV1` |
132
+ | Restriction Code (v1) | Restriction code (v1 element ID) | DAS | `restrictionCodeV1` |
133
+ | Endorsement Code (v1) | Endorsement code (v1 element ID) | DAT | `endorsementCodeV1` |
134
+ | Residence Street Address | Residence street address (v1) | DAL | `residenceStreetAddress` |
135
+ | Residence Street Address 2 | Residence street address line 2 (v1) | DAM | `residenceStreetAddress2` |
136
+ | Residence City | Residence city (v1) | DAN | `residenceCity` |
137
+ | Residence Jurisdiction Code | Residence state/province (v1) | DAO | `residenceJurisdictionCode` |
138
+ | Residence Postal Code | Residence postal code (v1) | DAP | `residencePostalCode` |
139
+ | AKA Last Name (v1) | AKA last name (v1 element ID) | DBO | `akaLastNameV1` |
140
+ | AKA First Name (v1) | AKA first name (v1 element ID) | DBP | `akaFirstNameV1` |
141
+ | AKA Middle Name | AKA middle name | DBQ | `akaMiddleName` |
142
+ | AKA Suffix (v1) | AKA suffix (v1 element ID) | DBR | `akaSuffixV1` |
143
+
144
+ #### V1 Fallback Behavior
145
+
146
+ When parsing v1 barcodes, the library normalizes v1 element IDs into their modern equivalents:
147
+
148
+ - `firstName` = DAC, then DCT, then parsed from DAA
149
+ - `lastName` = DCS, then DAB, then parsed from DAA
150
+ - `documentNumber` = DAQ, then DBJ
151
+ - `vehicleClass` = DCA, then DAR (trimmed)
152
+ - `restrictionCodes` = DCB, then DAS (trimmed)
153
+ - `endorsementCodes` = DCD, then DAT (trimmed)
154
+ - `lastNameAlias` = DBN, then DBO
155
+ - `firstNameAlias` = DBG, then DBP
156
+ - `organDonor` = DDK, then DBH
157
+
158
+ Space-padded v1 fields are automatically trimmed; all-whitespace values become `null`.
159
+
160
+ All fields return `null` when not present in the barcode data.
161
+
162
+ ### Additional Parsed Metadata
163
+
164
+ These fields are derived from the barcode header, not from element IDs:
165
+
166
+ | Name | Description | Type | `License` Attribute |
167
+ |:-----|:------------|:-----|:--------------------|
168
+ | Issuer ID | 6-digit IIN of the issuing jurisdiction | `number` | `issuerId` |
169
+ | AAMVA Version | AAMVA version number (1-11) | `number` | `aamvaVersion` |
170
+ | Jurisdiction | Full name of the issuing jurisdiction | `string` | `jurisdiction` |
171
+ | Raw Elements | All element key-value pairs from the barcode | `Record<string, string>` | `raw` |
172
+
173
+ ### AAMVA Element IDs by Version
174
+
175
+ **Bold** = mandatory in that version. `--` = not included. New in v11: DDM, DDN, DDO, DDP. Removed in v11: DCL, DDC, DBN, DBG, DBS.
176
+
177
+ | Field | v1 | v2 | v3 | v4 | v5 | v6 | v7 | v8 | v9 | v10 | v11 |
178
+ |:------|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:---:|:---:|
179
+ | First Name | DAC | **DCT** | **DCT** | **DAC** | **DAC** | **DAC** | **DAC** | **DAC** | **DAC** | **DAC** | **DAC** |
180
+ | Last Name | DAB | **DCS** | **DCS** | **DCS** | **DCS** | **DCS** | **DCS** | **DCS** | **DCS** | **DCS** | **DCS** |
181
+ | Middle Name | DAD | **DAD** | **DAD** | **DAD** | **DAD** | **DAD** | **DAD** | **DAD** | **DAD** | **DAD** | **DAD** |
182
+ | Name Suffix | DBN | **DCU** | DCU | DCU | DCU | DCU | DCU | DCU | DCU | DCU | DCU |
183
+ | Date of Birth | **DBB** | **DBB** | **DBB** | **DBB** | **DBB** | **DBB** | **DBB** | **DBB** | **DBB** | **DBB** | **DBB** |
184
+ | Expiration Date | **DBA** | **DBA** | **DBA** | **DBA** | **DBA** | **DBA** | **DBA** | **DBA** | **DBA** | **DBA** | **DBA** |
185
+ | Issue Date | **DBD** | **DBD** | **DBD** | **DBD** | **DBD** | **DBD** | **DBD** | **DBD** | **DBD** | **DBD** | **DBD** |
186
+ | Sex | **DBC** | **DBC** | **DBC** | **DBC** | **DBC** | **DBC** | **DBC** | **DBC** | **DBC** | **DBC** | **DBC** |
187
+ | Eye Color | DAY | **DAY** | **DAY** | **DAY** | **DAY** | **DAY** | **DAY** | **DAY** | **DAY** | **DAY** | **DAY** |
188
+ | Height | DAU | **DAU** | **DAU** | **DAU** | **DAU** | **DAU** | **DAU** | **DAU** | **DAU** | **DAU** | **DAU** |
189
+ | Street Address | **DAG** | **DAG** | **DAG** | **DAG** | **DAG** | **DAG** | **DAG** | **DAG** | **DAG** | **DAG** | **DAG** |
190
+ | Street Address 2 | DAH | DAH | DAH | DAH | DAH | DAH | DAH | DAH | DAH | DAH | DAH |
191
+ | City | **DAI** | **DAI** | **DAI** | **DAI** | **DAI** | **DAI** | **DAI** | **DAI** | **DAI** | **DAI** | **DAI** |
192
+ | State | **DAJ** | **DAJ** | **DAJ** | **DAJ** | **DAJ** | **DAJ** | **DAJ** | **DAJ** | **DAJ** | **DAJ** | **DAJ** |
193
+ | Postal Code | **DAK** | **DAK** | **DAK** | **DAK** | **DAK** | **DAK** | **DAK** | **DAK** | **DAK** | **DAK** | **DAK** |
194
+ | Country | `--` | **DCG** | **DCG** | **DCG** | **DCG** | **DCG** | **DCG** | **DCG** | **DCG** | **DCG** | **DCG** |
195
+ | Document Number | **DAQ** | **DAQ** | **DAQ** | **DAQ** | **DAQ** | **DAQ** | **DAQ** | **DAQ** | **DAQ** | **DAQ** | **DAQ** |
196
+ | Document Discriminator | `--` | **DCF** | **DCF** | **DCF** | **DCF** | **DCF** | **DCF** | **DCF** | **DCF** | **DCF** | **DCF** |
197
+ | Compliance Type | `--` | `--` | `--` | DDA | DDA | DDA | DDA | DDA | DDA | DDA | DDA |
198
+ | Card Revision Date | `--` | `--` | `--` | DDB | DDB | DDB | DDB | DDB | DDB | DDB | DDB |
199
+ | Limited Duration | `--` | `--` | `--` | DDD | DDD | DDD | DDD | DDD | DDD | DDD | DDD |
200
+ | Organ Donor | `--` | `--` | `--` | `--` | `--` | `--` | `--` | DDK | DDK | DDK | DDK |
201
+ | Veteran | `--` | `--` | `--` | `--` | `--` | `--` | `--` | DDL | DDL | DDL | DDL |
202
+ | First Name Truncation | `--` | **DDF** | `--` | **DDF** | **DDF** | **DDF** | **DDF** | **DDF** | **DDF** | **DDF** | **DDF** |
203
+ | Middle Name Truncation | `--` | **DDG** | `--` | **DDG** | **DDG** | **DDG** | **DDG** | **DDG** | **DDG** | **DDG** | **DDG** |
204
+ | Last Name Truncation | `--` | **DDE** | `--` | **DDE** | **DDE** | **DDE** | **DDE** | **DDE** | **DDE** | **DDE** | **DDE** |
205
+ | Place of Birth | `--` | `--` | DCI | DCI | DCI | DCI | DCI | DCI | DCI | DCI | DCI |
206
+ | Audit Information | `--` | `--` | DCJ | DCJ | DCJ | DCJ | DCJ | DCJ | DCJ | DCJ | DCJ |
207
+ | Inventory Control | `--` | `--` | DCK | DCK | DCK | DCK | DCK | DCK | DCK | DCK | DCK |
208
+ | Hair Color | DAZ | DAZ | DAZ | DAZ | DAZ | DAZ | DAZ | DAZ | DAZ | DAZ | DAZ |
209
+ | Weight | DAW | DAW | DAW | DAW | DAW | DAW | DAW | DAW | DAW | DAW | DAW |
210
+ | Last Name Alias | DBO | DBN | DBN | DBN | DBN | DBN | DBN | DBN | DBN | DBN | `--` |
211
+ | First Name Alias | DBP | DBG | DBG | DBG | DBG | DBG | DBG | DBG | DBG | DBG | `--` |
212
+ | Suffix Alias | DBR | `--` | DBS | DBS | DBS | DBS | DBS | DBS | DBS | DBS | `--` |
213
+ | Race/Ethnicity | `--` | `--` | DCL | DCL | DCL | DCL | DCL | DCL | DCL | DCL | `--` |
214
+ | HAZMAT Endorsement Exp. | `--` | `--` | `--` | DDC | DDC | DDC | DDC | DDC | DDC | DDC | `--` |
215
+ | CDL Indicator | `--` | `--` | `--` | `--` | `--` | `--` | `--` | `--` | `--` | `--` | DDM |
216
+ | Non-Domiciled Indicator | `--` | `--` | `--` | `--` | `--` | `--` | `--` | `--` | `--` | `--` | DDN |
217
+ | Enhanced Document | `--` | `--` | `--` | `--` | `--` | `--` | `--` | `--` | `--` | `--` | DDO |
218
+ | Permit Indicator | `--` | `--` | `--` | `--` | `--` | `--` | `--` | `--` | `--` | `--` | DDP |
219
+
220
+ ## Low-Level API
221
+
222
+ For advanced use cases, you can parse the raw barcode structure:
223
+
224
+ ```ts
225
+ import { parseBarcodeString } from "aamva-decoder";
226
+
227
+ const barcode = parseBarcodeString(barcodeData);
228
+ console.log(barcode.header.aamvaVersion); // 11
229
+ console.log(barcode.header.issuerId); // 636014
230
+
231
+ for (const subfile of barcode.subfiles) {
232
+ console.log(subfile.subfileType); // "DL", "ZC", etc.
233
+ console.log(subfile.elements); // Record<string, string>
234
+ }
235
+ ```
236
+
237
+ ## Utility Exports
238
+
239
+ ### `parseWeightRange(code: string): WeightRange`
240
+
241
+ Parses a weight range code (0-9) from the DCE element into a descriptive object:
242
+
243
+ ```ts
244
+ import { parseWeightRange } from "aamva-decoder";
245
+
246
+ const range = parseWeightRange("5");
247
+ // { code: "5", description: "191-220 lbs (87-100 kg)", lbs: "191-220", kg: "87-100" }
248
+ ```
249
+
250
+ The `WEIGHT_RANGES` constant array is also exported for enumeration.
251
+
252
+ ## License
253
+
254
+ MIT
@@ -0,0 +1,30 @@
1
+ export declare const COMPLIANCE_INDICATOR = "@";
2
+ export declare const DATA_ELEMENT_SEPARATOR = "\n";
3
+ export declare const RECORD_SEPARATOR = "\u001E";
4
+ export declare const SEGMENT_TERMINATOR = "\r";
5
+ export declare const FILE_TYPE = "ANSI ";
6
+ export interface FileHeader {
7
+ issuerId: number;
8
+ aamvaVersion: number;
9
+ numberOfEntries: number;
10
+ jurisdictionVersion: number;
11
+ }
12
+ export interface SubfileDesignator {
13
+ subfileType: string;
14
+ offset: number;
15
+ length: number;
16
+ }
17
+ export interface Subfile {
18
+ subfileType: string;
19
+ elements: Record<string, string>;
20
+ }
21
+ export interface BarcodeFile {
22
+ header: FileHeader;
23
+ subfiles: Subfile[];
24
+ }
25
+ export declare function trimBefore(char: string, str: string): string;
26
+ export declare function headerLength(aamvaVersion: number): 19 | 21;
27
+ export declare function parseFileHeader(barcodeString: string): FileHeader;
28
+ export declare function parseSubfileDesignator(barcodeString: string, aamvaVersion: number, designatorIndex: number): SubfileDesignator;
29
+ export declare function parseSubfile(barcodeString: string, designator: SubfileDesignator): Subfile;
30
+ export declare function parseBarcodeString(barcodeString: string): BarcodeFile;
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FILE_TYPE = exports.SEGMENT_TERMINATOR = exports.RECORD_SEPARATOR = exports.DATA_ELEMENT_SEPARATOR = exports.COMPLIANCE_INDICATOR = void 0;
4
+ exports.trimBefore = trimBefore;
5
+ exports.headerLength = headerLength;
6
+ exports.parseFileHeader = parseFileHeader;
7
+ exports.parseSubfileDesignator = parseSubfileDesignator;
8
+ exports.parseSubfile = parseSubfile;
9
+ exports.parseBarcodeString = parseBarcodeString;
10
+ exports.COMPLIANCE_INDICATOR = "@";
11
+ exports.DATA_ELEMENT_SEPARATOR = "\n";
12
+ exports.RECORD_SEPARATOR = "\x1e";
13
+ exports.SEGMENT_TERMINATOR = "\r";
14
+ exports.FILE_TYPE = "ANSI ";
15
+ function trimBefore(char, str) {
16
+ const index = str.indexOf(char);
17
+ return index === -1 ? str : str.slice(index);
18
+ }
19
+ function headerLength(aamvaVersion) {
20
+ if (aamvaVersion < 1 || aamvaVersion > 99) {
21
+ throw new Error("aamva_version is out of range (1-99).");
22
+ }
23
+ return aamvaVersion < 2 ? 19 : 21;
24
+ }
25
+ function parseFileHeader(barcodeString) {
26
+ const MIN_LENGTH = 17;
27
+ if (barcodeString.length < MIN_LENGTH) {
28
+ throw new Error("Header length is too short.");
29
+ }
30
+ else if (barcodeString[0] !== exports.COMPLIANCE_INDICATOR) {
31
+ throw new Error("Header element 'COMPLIANCE_INDICATOR' is invalid.");
32
+ }
33
+ else if (barcodeString[1] !== exports.DATA_ELEMENT_SEPARATOR) {
34
+ throw new Error("Header element 'DATA_ELEMENT_SEPARATOR' is invalid.");
35
+ }
36
+ else if (barcodeString[2] !== exports.RECORD_SEPARATOR) {
37
+ throw new Error("Header element 'RECORD_SEPARATOR' is invalid.");
38
+ }
39
+ else if (barcodeString[3] !== exports.SEGMENT_TERMINATOR) {
40
+ throw new Error("Header element 'SEGMENT_TERMINATOR' is invalid.");
41
+ }
42
+ else if (barcodeString.slice(4, 9) !== exports.FILE_TYPE) {
43
+ throw new Error("Header element 'FILE_TYPE' is invalid.");
44
+ }
45
+ const aamvaVersion = parseInt(barcodeString.slice(15, 17), 10);
46
+ if (barcodeString.length < headerLength(aamvaVersion)) {
47
+ throw new Error("Header length is too short.");
48
+ }
49
+ const issuerId = parseInt(barcodeString.slice(9, 15), 10);
50
+ const numberOfEntries = parseInt(aamvaVersion < 2 ? barcodeString.slice(17, 19) : barcodeString.slice(19, 21), 10);
51
+ const jurisdictionVersion = aamvaVersion < 2 ? 0 : parseInt(barcodeString.slice(17, 19), 10);
52
+ return {
53
+ issuerId,
54
+ aamvaVersion,
55
+ numberOfEntries,
56
+ jurisdictionVersion,
57
+ };
58
+ }
59
+ function parseSubfileDesignator(barcodeString, aamvaVersion, designatorIndex) {
60
+ const DESIGNATOR_LENGTH = 10;
61
+ const cursor = designatorIndex * DESIGNATOR_LENGTH + headerLength(aamvaVersion);
62
+ if (barcodeString.length < cursor + DESIGNATOR_LENGTH) {
63
+ throw new Error("Subfile designator is too short.");
64
+ }
65
+ return {
66
+ subfileType: barcodeString.slice(cursor, cursor + 2),
67
+ offset: parseInt(barcodeString.slice(cursor + 2, cursor + 6), 10),
68
+ length: parseInt(barcodeString.slice(cursor + 6, cursor + 10), 10),
69
+ };
70
+ }
71
+ function parseSubfile(barcodeString, designator) {
72
+ const { subfileType, offset, length } = designator;
73
+ const endOffset = offset + length;
74
+ if (barcodeString.length < endOffset) {
75
+ throw new Error("Subfile length is too short.");
76
+ }
77
+ else if (barcodeString.slice(offset, offset + 2) !== subfileType) {
78
+ throw new Error("Subfile is missing subfile type.");
79
+ }
80
+ else if (barcodeString[endOffset - 1] !== exports.SEGMENT_TERMINATOR) {
81
+ throw new Error("Subfile is missing segment terminator.");
82
+ }
83
+ const items = barcodeString
84
+ .slice(offset + 2, endOffset - 1)
85
+ .split(exports.DATA_ELEMENT_SEPARATOR)
86
+ .filter(Boolean);
87
+ const elements = {};
88
+ for (const item of items) {
89
+ elements[item.slice(0, 3)] = item.slice(3);
90
+ }
91
+ return { subfileType, elements };
92
+ }
93
+ function parseBarcodeString(barcodeString) {
94
+ barcodeString = trimBefore(exports.COMPLIANCE_INDICATOR, barcodeString);
95
+ const header = parseFileHeader(barcodeString);
96
+ if (header.numberOfEntries < 1) {
97
+ throw new Error("Number of entries cannot be less than 1.");
98
+ }
99
+ const subfiles = [];
100
+ for (let i = 0; i < header.numberOfEntries; i++) {
101
+ const designator = parseSubfileDesignator(barcodeString, header.aamvaVersion, i);
102
+ const subfile = parseSubfile(barcodeString, designator);
103
+ subfiles.push(subfile);
104
+ }
105
+ return { header, subfiles };
106
+ }
@@ -0,0 +1,6 @@
1
+ export declare const ISO_FORMAT: "ISO";
2
+ export declare const IMPERIAL_FORMAT: "IMPERIAL";
3
+ export type DateFormat = typeof ISO_FORMAT | typeof IMPERIAL_FORMAT;
4
+ export declare function countryDateFormat(country: string): DateFormat;
5
+ export declare function getDateFormat(aamvaVersion: number, country: string): DateFormat;
6
+ export declare function parseDate(dateString: string, format: DateFormat): Date;
package/dist/dates.js ADDED
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IMPERIAL_FORMAT = exports.ISO_FORMAT = void 0;
4
+ exports.countryDateFormat = countryDateFormat;
5
+ exports.getDateFormat = getDateFormat;
6
+ exports.parseDate = parseDate;
7
+ exports.ISO_FORMAT = "ISO";
8
+ exports.IMPERIAL_FORMAT = "IMPERIAL";
9
+ function countryDateFormat(country) {
10
+ country = country.toUpperCase();
11
+ if (country === "CANADA")
12
+ return exports.ISO_FORMAT;
13
+ if (country === "MEXICO")
14
+ return exports.ISO_FORMAT;
15
+ if (country === "USA")
16
+ return exports.IMPERIAL_FORMAT;
17
+ throw new Error("Provided country is not supported.");
18
+ }
19
+ function getDateFormat(aamvaVersion, country) {
20
+ return aamvaVersion < 3 ? exports.IMPERIAL_FORMAT : countryDateFormat(country);
21
+ }
22
+ function parseDate(dateString, format) {
23
+ let year;
24
+ let month;
25
+ let day;
26
+ if (format === exports.ISO_FORMAT) {
27
+ // YYYYMMDD
28
+ if (dateString.length !== 8) {
29
+ throw new Error("Invalid date format for provided date string.");
30
+ }
31
+ year = parseInt(dateString.slice(0, 4), 10);
32
+ month = parseInt(dateString.slice(4, 6), 10);
33
+ day = parseInt(dateString.slice(6, 8), 10);
34
+ }
35
+ else {
36
+ // MMDDYYYY
37
+ if (dateString.length !== 8) {
38
+ throw new Error("Invalid date format for provided date string.");
39
+ }
40
+ month = parseInt(dateString.slice(0, 2), 10);
41
+ day = parseInt(dateString.slice(2, 4), 10);
42
+ year = parseInt(dateString.slice(4, 8), 10);
43
+ }
44
+ if (isNaN(year) || isNaN(month) || isNaN(day)) {
45
+ throw new Error("Invalid date format for provided date string.");
46
+ }
47
+ const date = new Date(year, month - 1, day);
48
+ // Validate the date components round-trip correctly
49
+ if (date.getFullYear() !== year ||
50
+ date.getMonth() !== month - 1 ||
51
+ date.getDate() !== day) {
52
+ throw new Error("Invalid date format for provided date string.");
53
+ }
54
+ return date;
55
+ }
@@ -0,0 +1,7 @@
1
+ export interface EyeColor {
2
+ code: string;
3
+ color: string;
4
+ description: string;
5
+ }
6
+ export declare const EYE_COLORS: readonly EyeColor[];
7
+ export declare function parseEyeColor(code: string): EyeColor;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EYE_COLORS = void 0;
4
+ exports.parseEyeColor = parseEyeColor;
5
+ exports.EYE_COLORS = [
6
+ { code: "BLK", color: "Black", description: "Black or very dark brown" },
7
+ { code: "BLU", color: "Blue", description: "Blue" },
8
+ { code: "BRO", color: "Brown", description: "Brown, including amber" },
9
+ {
10
+ code: "DIC",
11
+ color: "Dichromatic",
12
+ description: "Dichromatic or multicolor, of one or both eyes",
13
+ },
14
+ { code: "GRY", color: "Gray", description: "Gray" },
15
+ { code: "GRN", color: "Green", description: "Green" },
16
+ {
17
+ code: "HAZ",
18
+ color: "Hazel",
19
+ description: "Hazel, a mixture of colors, most commonly green and brown",
20
+ },
21
+ { code: "MAR", color: "Maroon", description: "Maroon" },
22
+ { code: "PNK", color: "Pink", description: "Pink or albino" },
23
+ { code: "UNK", color: "Unknown", description: "Unknown" },
24
+ ];
25
+ function parseEyeColor(code) {
26
+ code = code === "BRN" ? "BRO" : code;
27
+ const found = exports.EYE_COLORS.find((c) => c.code === code);
28
+ if (!found) {
29
+ throw new Error(`Color code '${code}' not found.`);
30
+ }
31
+ return found;
32
+ }
@@ -0,0 +1,6 @@
1
+ export interface HairColor {
2
+ code: string;
3
+ color: string;
4
+ }
5
+ export declare const HAIR_COLORS: readonly HairColor[];
6
+ export declare function parseHairColor(code: string): HairColor;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HAIR_COLORS = void 0;
4
+ exports.parseHairColor = parseHairColor;
5
+ exports.HAIR_COLORS = [
6
+ { code: "BAL", color: "Bald" },
7
+ { code: "BLK", color: "Black" },
8
+ { code: "BLN", color: "Blond" },
9
+ { code: "BRO", color: "Brown" },
10
+ { code: "GRY", color: "Gray" },
11
+ { code: "RED", color: "Red/Auburn" },
12
+ { code: "SDY", color: "Sandy" },
13
+ { code: "WHI", color: "White" },
14
+ { code: "UNK", color: "Unknown" },
15
+ ];
16
+ function parseHairColor(code) {
17
+ code = code === "BRN" ? "BRO" : code;
18
+ const found = exports.HAIR_COLORS.find((c) => c.code === code);
19
+ if (!found) {
20
+ throw new Error(`Color code '${code}' not found.`);
21
+ }
22
+ return found;
23
+ }
@@ -0,0 +1,8 @@
1
+ export { parseBarcodeString, parseFileHeader, parseSubfileDesignator, parseSubfile, trimBefore, headerLength, type BarcodeFile, type FileHeader, type Subfile, type SubfileDesignator, } from "./barcode";
2
+ export { parseLicense, parseLicenseFromBarcode, type License } from "./license";
3
+ export { parseWeightRange, type WeightRange, WEIGHT_RANGES, } from "./weightRange";
4
+ export { getDateFormat, countryDateFormat, parseDate, type DateFormat, } from "./dates";
5
+ export { parseEyeColor, type EyeColor, EYE_COLORS } from "./eyeColor";
6
+ export { parseHairColor, type HairColor, HAIR_COLORS } from "./hairColor";
7
+ export { parseRaceEthnicity, type RaceEthnicity, RACE_ETHNICITIES, } from "./raceEthnicity";
8
+ export { getAuthorityById, type IssuingAuthority, ISSUING_AUTHORITIES, } from "./issuingAuthority";
package/dist/index.js ADDED
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ISSUING_AUTHORITIES = exports.getAuthorityById = exports.RACE_ETHNICITIES = exports.parseRaceEthnicity = exports.HAIR_COLORS = exports.parseHairColor = exports.EYE_COLORS = exports.parseEyeColor = exports.parseDate = exports.countryDateFormat = exports.getDateFormat = exports.WEIGHT_RANGES = exports.parseWeightRange = exports.parseLicenseFromBarcode = exports.parseLicense = exports.headerLength = exports.trimBefore = exports.parseSubfile = exports.parseSubfileDesignator = exports.parseFileHeader = exports.parseBarcodeString = void 0;
4
+ var barcode_1 = require("./barcode");
5
+ Object.defineProperty(exports, "parseBarcodeString", { enumerable: true, get: function () { return barcode_1.parseBarcodeString; } });
6
+ Object.defineProperty(exports, "parseFileHeader", { enumerable: true, get: function () { return barcode_1.parseFileHeader; } });
7
+ Object.defineProperty(exports, "parseSubfileDesignator", { enumerable: true, get: function () { return barcode_1.parseSubfileDesignator; } });
8
+ Object.defineProperty(exports, "parseSubfile", { enumerable: true, get: function () { return barcode_1.parseSubfile; } });
9
+ Object.defineProperty(exports, "trimBefore", { enumerable: true, get: function () { return barcode_1.trimBefore; } });
10
+ Object.defineProperty(exports, "headerLength", { enumerable: true, get: function () { return barcode_1.headerLength; } });
11
+ var license_1 = require("./license");
12
+ Object.defineProperty(exports, "parseLicense", { enumerable: true, get: function () { return license_1.parseLicense; } });
13
+ Object.defineProperty(exports, "parseLicenseFromBarcode", { enumerable: true, get: function () { return license_1.parseLicenseFromBarcode; } });
14
+ var weightRange_1 = require("./weightRange");
15
+ Object.defineProperty(exports, "parseWeightRange", { enumerable: true, get: function () { return weightRange_1.parseWeightRange; } });
16
+ Object.defineProperty(exports, "WEIGHT_RANGES", { enumerable: true, get: function () { return weightRange_1.WEIGHT_RANGES; } });
17
+ var dates_1 = require("./dates");
18
+ Object.defineProperty(exports, "getDateFormat", { enumerable: true, get: function () { return dates_1.getDateFormat; } });
19
+ Object.defineProperty(exports, "countryDateFormat", { enumerable: true, get: function () { return dates_1.countryDateFormat; } });
20
+ Object.defineProperty(exports, "parseDate", { enumerable: true, get: function () { return dates_1.parseDate; } });
21
+ var eyeColor_1 = require("./eyeColor");
22
+ Object.defineProperty(exports, "parseEyeColor", { enumerable: true, get: function () { return eyeColor_1.parseEyeColor; } });
23
+ Object.defineProperty(exports, "EYE_COLORS", { enumerable: true, get: function () { return eyeColor_1.EYE_COLORS; } });
24
+ var hairColor_1 = require("./hairColor");
25
+ Object.defineProperty(exports, "parseHairColor", { enumerable: true, get: function () { return hairColor_1.parseHairColor; } });
26
+ Object.defineProperty(exports, "HAIR_COLORS", { enumerable: true, get: function () { return hairColor_1.HAIR_COLORS; } });
27
+ var raceEthnicity_1 = require("./raceEthnicity");
28
+ Object.defineProperty(exports, "parseRaceEthnicity", { enumerable: true, get: function () { return raceEthnicity_1.parseRaceEthnicity; } });
29
+ Object.defineProperty(exports, "RACE_ETHNICITIES", { enumerable: true, get: function () { return raceEthnicity_1.RACE_ETHNICITIES; } });
30
+ var issuingAuthority_1 = require("./issuingAuthority");
31
+ Object.defineProperty(exports, "getAuthorityById", { enumerable: true, get: function () { return issuingAuthority_1.getAuthorityById; } });
32
+ Object.defineProperty(exports, "ISSUING_AUTHORITIES", { enumerable: true, get: function () { return issuingAuthority_1.ISSUING_AUTHORITIES; } });
@@ -0,0 +1,8 @@
1
+ export interface IssuingAuthority {
2
+ issuerId: number;
3
+ jurisdiction: string;
4
+ abbr: string | null;
5
+ country: string;
6
+ }
7
+ export declare const ISSUING_AUTHORITIES: readonly IssuingAuthority[];
8
+ export declare function getAuthorityById(idNumber: number): IssuingAuthority;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ISSUING_AUTHORITIES = void 0;
4
+ exports.getAuthorityById = getAuthorityById;
5
+ exports.ISSUING_AUTHORITIES = [
6
+ { issuerId: 604426, jurisdiction: "Prince Edward Island", abbr: "PE", country: "Canada" },
7
+ { issuerId: 604427, jurisdiction: "American Samoa", abbr: "AS", country: "USA" },
8
+ { issuerId: 604428, jurisdiction: "Quebec", abbr: "QC", country: "Canada" },
9
+ { issuerId: 604429, jurisdiction: "Yukon", abbr: "YT", country: "Canada" },
10
+ { issuerId: 604430, jurisdiction: "Norther Marianna Islands", abbr: "MP", country: "USA" },
11
+ { issuerId: 604431, jurisdiction: "Puerto Rico", abbr: "PR", country: "USA" },
12
+ { issuerId: 604432, jurisdiction: "Alberta", abbr: "AB", country: "Canada" },
13
+ { issuerId: 604433, jurisdiction: "Nunavut", abbr: "NU", country: "Canada" },
14
+ { issuerId: 604434, jurisdiction: "Northwest Territories", abbr: "NT", country: "Canada" },
15
+ { issuerId: 636000, jurisdiction: "Virginia", abbr: "VA", country: "USA" },
16
+ { issuerId: 636001, jurisdiction: "New York", abbr: "NY", country: "USA" },
17
+ { issuerId: 636002, jurisdiction: "Massachusetts", abbr: "MA", country: "USA" },
18
+ { issuerId: 636003, jurisdiction: "Maryland", abbr: "MD", country: "USA" },
19
+ { issuerId: 636004, jurisdiction: "North Carolina", abbr: "NC", country: "USA" },
20
+ { issuerId: 636005, jurisdiction: "South Carolina", abbr: "SC", country: "USA" },
21
+ { issuerId: 636006, jurisdiction: "Connecticut", abbr: "CT", country: "USA" },
22
+ { issuerId: 636007, jurisdiction: "Louisiana", abbr: "LA", country: "USA" },
23
+ { issuerId: 636008, jurisdiction: "Montana", abbr: "MT", country: "USA" },
24
+ { issuerId: 636009, jurisdiction: "New Mexico", abbr: "NM", country: "USA" },
25
+ { issuerId: 636010, jurisdiction: "Florida", abbr: "FL", country: "USA" },
26
+ { issuerId: 636011, jurisdiction: "Delaware", abbr: "DE", country: "USA" },
27
+ { issuerId: 636012, jurisdiction: "Ontario", abbr: "ON", country: "Canada" },
28
+ { issuerId: 636013, jurisdiction: "Nova Scotia", abbr: "NS", country: "Canada" },
29
+ { issuerId: 636014, jurisdiction: "California", abbr: "CA", country: "USA" },
30
+ { issuerId: 636015, jurisdiction: "Texas", abbr: "TX", country: "USA" },
31
+ { issuerId: 636016, jurisdiction: "Newfoundland", abbr: "NF", country: "Canada" },
32
+ { issuerId: 636017, jurisdiction: "New Brunswick", abbr: "NB", country: "Canada" },
33
+ { issuerId: 636018, jurisdiction: "Iowa", abbr: "IA", country: "USA" },
34
+ { issuerId: 636019, jurisdiction: "Guam", abbr: "GU", country: "USA" },
35
+ { issuerId: 636020, jurisdiction: "Colorado", abbr: "GM", country: "USA" },
36
+ { issuerId: 636021, jurisdiction: "Arkansas", abbr: "AR", country: "USA" },
37
+ { issuerId: 636022, jurisdiction: "Kansas", abbr: "KS", country: "USA" },
38
+ { issuerId: 636023, jurisdiction: "Ohio", abbr: "OH", country: "USA" },
39
+ { issuerId: 636024, jurisdiction: "Vermont", abbr: "VT", country: "USA" },
40
+ { issuerId: 636025, jurisdiction: "Pennsylvania", abbr: "PA", country: "USA" },
41
+ { issuerId: 636026, jurisdiction: "Arizona", abbr: "AZ", country: "USA" },
42
+ { issuerId: 636027, jurisdiction: "State Dept. (Diplomatic)", abbr: null, country: "USA" },
43
+ { issuerId: 636028, jurisdiction: "British Columbia", abbr: "BC", country: "Canada" },
44
+ { issuerId: 636029, jurisdiction: "Oregon", abbr: "OR", country: "USA" },
45
+ { issuerId: 636030, jurisdiction: "Missouri", abbr: "MO", country: "USA" },
46
+ { issuerId: 636031, jurisdiction: "Wisconsin", abbr: "WI", country: "USA" },
47
+ { issuerId: 636032, jurisdiction: "Michigan", abbr: "MI", country: "USA" },
48
+ { issuerId: 636033, jurisdiction: "Alabama", abbr: "AL", country: "USA" },
49
+ { issuerId: 636034, jurisdiction: "North Dakota", abbr: "ND", country: "USA" },
50
+ { issuerId: 636035, jurisdiction: "Illinois", abbr: "IL", country: "USA" },
51
+ { issuerId: 636036, jurisdiction: "New Jersey", abbr: "NJ", country: "USA" },
52
+ { issuerId: 636037, jurisdiction: "Indiana", abbr: "IN", country: "USA" },
53
+ { issuerId: 636038, jurisdiction: "Minnesota", abbr: "MN", country: "USA" },
54
+ { issuerId: 636039, jurisdiction: "New Hampshire", abbr: "NH", country: "USA" },
55
+ { issuerId: 636040, jurisdiction: "Utah", abbr: "UT", country: "USA" },
56
+ { issuerId: 636041, jurisdiction: "Maine", abbr: "ME", country: "USA" },
57
+ { issuerId: 636042, jurisdiction: "South Dakota", abbr: "SD", country: "USA" },
58
+ { issuerId: 636043, jurisdiction: "District of Columbia", abbr: "DC", country: "USA" },
59
+ { issuerId: 636044, jurisdiction: "Saskatchewan", abbr: "SK", country: "Canada" },
60
+ { issuerId: 636045, jurisdiction: "Washington", abbr: "WA", country: "USA" },
61
+ { issuerId: 636046, jurisdiction: "Kentucky", abbr: "KY", country: "USA" },
62
+ { issuerId: 636047, jurisdiction: "Hawaii", abbr: "HI", country: "USA" },
63
+ { issuerId: 636048, jurisdiction: "Manitoba", abbr: "MB", country: "Canada" },
64
+ { issuerId: 636049, jurisdiction: "Nevada", abbr: "NV", country: "USA" },
65
+ { issuerId: 636050, jurisdiction: "Idaho", abbr: "ID", country: "USA" },
66
+ { issuerId: 636051, jurisdiction: "Mississippi", abbr: "MS", country: "USA" },
67
+ { issuerId: 636052, jurisdiction: "Rhode Island", abbr: "RI", country: "USA" },
68
+ { issuerId: 636053, jurisdiction: "Tennessee", abbr: "TN", country: "USA" },
69
+ { issuerId: 636054, jurisdiction: "Nebraska", abbr: "NE", country: "USA" },
70
+ { issuerId: 636055, jurisdiction: "Georgia", abbr: "GA", country: "USA" },
71
+ { issuerId: 636056, jurisdiction: "Coahuila", abbr: "CU", country: "Mexico" },
72
+ { issuerId: 636057, jurisdiction: "Hidalgo", abbr: "HL", country: "Mexico" },
73
+ { issuerId: 636058, jurisdiction: "Oklahoma", abbr: "OK", country: "USA" },
74
+ { issuerId: 636059, jurisdiction: "Alaska", abbr: "AK", country: "USA" },
75
+ { issuerId: 636060, jurisdiction: "Wyoming", abbr: "WY", country: "USA" },
76
+ { issuerId: 636061, jurisdiction: "West Virginia", abbr: "WV", country: "USA" },
77
+ { issuerId: 636062, jurisdiction: "Virgin Islands", abbr: "VI", country: "USA" },
78
+ ];
79
+ function getAuthorityById(idNumber) {
80
+ const found = exports.ISSUING_AUTHORITIES.find((a) => a.issuerId === idNumber);
81
+ if (!found) {
82
+ throw new Error(`Issuer ID number '${idNumber}' not found in authority list.`);
83
+ }
84
+ return found;
85
+ }
@@ -0,0 +1,89 @@
1
+ import { type BarcodeFile } from "./barcode";
2
+ export interface License {
3
+ firstName: string | null;
4
+ lastName: string | null;
5
+ middleName: string | null;
6
+ nameSuffix: string | null;
7
+ dateOfBirth: Date | null;
8
+ expirationDate: Date | null;
9
+ issueDate: Date | null;
10
+ streetAddress: string | null;
11
+ city: string | null;
12
+ state: string | null;
13
+ postalCode: string | null;
14
+ country: string | null;
15
+ documentNumber: string | null;
16
+ documentDiscriminator: string | null;
17
+ sex: "Male" | "Female" | "Not specified" | null;
18
+ height: string | null;
19
+ weight: string | null;
20
+ eyeColor: string | null;
21
+ hairColor: string | null;
22
+ streetAddress2: string | null;
23
+ complianceType: string | null;
24
+ cardRevisionDate: Date | null;
25
+ limitedDurationDocument: boolean | null;
26
+ organDonor: boolean | null;
27
+ veteran: boolean | null;
28
+ firstNameTruncation: string | null;
29
+ middleNameTruncation: string | null;
30
+ lastNameTruncation: string | null;
31
+ placeOfBirth: string | null;
32
+ auditInformation: string | null;
33
+ inventoryControlNumber: string | null;
34
+ lastNameAlias: string | null;
35
+ firstNameAlias: string | null;
36
+ suffixAlias: string | null;
37
+ raceEthnicity: string | null;
38
+ hazmatEndorsementExpiration: Date | null;
39
+ cdlIndicator: boolean | null;
40
+ nonDomiciledIndicator: boolean | null;
41
+ enhancedDocumentIndicator: boolean | null;
42
+ permitIndicator: boolean | null;
43
+ vehicleClass: string | null;
44
+ restrictionCodes: string | null;
45
+ endorsementCodes: string | null;
46
+ heightCm: string | null;
47
+ weightKg: string | null;
48
+ weightRange: string | null;
49
+ federalCommercialVehicleCodes: string | null;
50
+ standardVehicleClassification: string | null;
51
+ standardEndorsementCode: string | null;
52
+ standardRestrictionCode: string | null;
53
+ vehicleClassDescription: string | null;
54
+ endorsementCodeDescription: string | null;
55
+ restrictionCodeDescription: string | null;
56
+ namePrefix: string | null;
57
+ firstNameV2: string | null;
58
+ lastNameV1: string | null;
59
+ nameSuffixV1: string | null;
60
+ classificationCodeV1: string | null;
61
+ restrictionCodeV1: string | null;
62
+ endorsementCodeV1: string | null;
63
+ residenceStreetAddress: string | null;
64
+ residenceStreetAddress2: string | null;
65
+ residenceCity: string | null;
66
+ residenceJurisdictionCode: string | null;
67
+ residencePostalCode: string | null;
68
+ issueTimestamp: string | null;
69
+ numberOfDuplicates: string | null;
70
+ organDonorLegacy: string | null;
71
+ nonResidentIndicator: string | null;
72
+ uniqueCustomerId: string | null;
73
+ socialSecurityNumber: string | null;
74
+ akaDateOfBirth: Date | null;
75
+ akaSocialSecurityNumber: string | null;
76
+ akaLastNameV1: string | null;
77
+ akaFirstNameV1: string | null;
78
+ akaMiddleName: string | null;
79
+ akaSuffixV1: string | null;
80
+ under18Until: Date | null;
81
+ under19Until: Date | null;
82
+ under21Until: Date | null;
83
+ raw: Record<string, string>;
84
+ issuerId: number;
85
+ aamvaVersion: number;
86
+ jurisdiction: string | null;
87
+ }
88
+ export declare function parseLicenseFromBarcode(barcode: BarcodeFile): License;
89
+ export declare function parseLicense(barcodeString: string): License;
@@ -0,0 +1,263 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseLicenseFromBarcode = parseLicenseFromBarcode;
4
+ exports.parseLicense = parseLicense;
5
+ const barcode_1 = require("./barcode");
6
+ const dates_1 = require("./dates");
7
+ const eyeColor_1 = require("./eyeColor");
8
+ const hairColor_1 = require("./hairColor");
9
+ const issuingAuthority_1 = require("./issuingAuthority");
10
+ const raceEthnicity_1 = require("./raceEthnicity");
11
+ const weightRange_1 = require("./weightRange");
12
+ function get(elements, key) {
13
+ const val = elements[key];
14
+ return val !== undefined ? val : null;
15
+ }
16
+ /** Returns trimmed value or null for space-padded v1 fields. */
17
+ function getOrNull(elements, key) {
18
+ const val = elements[key];
19
+ if (val === undefined)
20
+ return null;
21
+ const trimmed = val.trim();
22
+ return trimmed.length > 0 ? trimmed : null;
23
+ }
24
+ function parseSex(value, aamvaVersion) {
25
+ if (value === null)
26
+ return null;
27
+ if (aamvaVersion < 2) {
28
+ // v1 used "M"/"F"
29
+ if (value === "M")
30
+ return "Male";
31
+ if (value === "F")
32
+ return "Female";
33
+ return "Not specified";
34
+ }
35
+ if (value === "1")
36
+ return "Male";
37
+ if (value === "2")
38
+ return "Female";
39
+ if (value === "9")
40
+ return "Not specified";
41
+ return null;
42
+ }
43
+ function tryParseDate(dateStr, aamvaVersion, country) {
44
+ if (!dateStr)
45
+ return null;
46
+ try {
47
+ const format = (0, dates_1.getDateFormat)(aamvaVersion, country);
48
+ return (0, dates_1.parseDate)(dateStr, format);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ function tryParseEyeColor(code) {
55
+ if (!code)
56
+ return null;
57
+ try {
58
+ return (0, eyeColor_1.parseEyeColor)(code.trim()).color;
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ function parseIndicator(value) {
65
+ if (value === null)
66
+ return null;
67
+ return value === "1";
68
+ }
69
+ function tryParseRaceEthnicity(code) {
70
+ if (!code)
71
+ return null;
72
+ try {
73
+ return (0, raceEthnicity_1.parseRaceEthnicity)(code.trim()).description;
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ function tryParseHairColor(code) {
80
+ if (!code)
81
+ return null;
82
+ try {
83
+ return (0, hairColor_1.parseHairColor)(code.trim()).color;
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ function tryParseWeightRange(code) {
90
+ if (!code)
91
+ return null;
92
+ try {
93
+ return (0, weightRange_1.parseWeightRange)(code.trim()).description;
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
99
+ function parseLicenseFromBarcode(barcode) {
100
+ const { header } = barcode;
101
+ // Find the DL or ID subfile
102
+ const dlSubfile = barcode.subfiles.find((s) => s.subfileType === "DL") ??
103
+ barcode.subfiles.find((s) => s.subfileType === "ID");
104
+ const elements = dlSubfile?.elements ?? {};
105
+ // Determine country for date formatting
106
+ let country = get(elements, "DCG") ?? "USA";
107
+ try {
108
+ const authority = (0, issuingAuthority_1.getAuthorityById)(header.issuerId);
109
+ country = authority.country;
110
+ }
111
+ catch {
112
+ // Use DCG or default
113
+ }
114
+ // Name parsing with v1/v2 fallbacks
115
+ let firstName;
116
+ let lastName;
117
+ let middleName;
118
+ if (header.aamvaVersion >= 3) {
119
+ firstName = get(elements, "DAC");
120
+ lastName = get(elements, "DCS");
121
+ middleName = get(elements, "DAD");
122
+ }
123
+ else {
124
+ // v1-2: parse from DAA (LAST,FIRST,MIDDLE), fallback to individual fields
125
+ const fullName = get(elements, "DAA");
126
+ if (fullName) {
127
+ const parts = fullName.split(",");
128
+ lastName = parts[0]?.trim() || null;
129
+ firstName = parts[1]?.trim() || null;
130
+ middleName = parts[2]?.trim() || null;
131
+ }
132
+ else {
133
+ firstName = get(elements, "DAC");
134
+ lastName = get(elements, "DCS");
135
+ middleName = get(elements, "DAD");
136
+ }
137
+ }
138
+ // V1/v2 fallback: DAC ?? DCT ?? parsed-from-DAA
139
+ firstName = firstName ?? getOrNull(elements, "DCT");
140
+ // V1 fallback: DCS ?? DAB ?? parsed-from-DAA
141
+ lastName = lastName ?? getOrNull(elements, "DAB");
142
+ // V1 fallback: DAQ ?? DBJ
143
+ const documentNumber = get(elements, "DAQ") ?? getOrNull(elements, "DBJ");
144
+ // Driving privilege with v1 fallbacks
145
+ const vehicleClass = getOrNull(elements, "DCA") ?? getOrNull(elements, "DAR");
146
+ const restrictionCodes = getOrNull(elements, "DCB") ?? getOrNull(elements, "DAS");
147
+ const endorsementCodes = getOrNull(elements, "DCD") ?? getOrNull(elements, "DAT");
148
+ // Alias fallbacks: v2+ IDs ?? v1 IDs
149
+ const lastNameAlias = get(elements, "DBN") ?? getOrNull(elements, "DBO");
150
+ const firstNameAlias = get(elements, "DBG") ?? getOrNull(elements, "DBP");
151
+ // Organ donor fallback: DDK ?? DBH
152
+ const organDonor = get(elements, "DDK") !== undefined
153
+ ? parseIndicator(get(elements, "DDK"))
154
+ : parseIndicator(getOrNull(elements, "DBH"));
155
+ // Jurisdiction
156
+ let jurisdiction = null;
157
+ try {
158
+ jurisdiction = (0, issuingAuthority_1.getAuthorityById)(header.issuerId).jurisdiction;
159
+ }
160
+ catch {
161
+ // Unknown issuer
162
+ }
163
+ return {
164
+ firstName,
165
+ lastName,
166
+ middleName,
167
+ nameSuffix: get(elements, "DCU"),
168
+ dateOfBirth: tryParseDate(get(elements, "DBB"), header.aamvaVersion, country),
169
+ expirationDate: tryParseDate(get(elements, "DBA"), header.aamvaVersion, country),
170
+ issueDate: tryParseDate(get(elements, "DBD"), header.aamvaVersion, country),
171
+ streetAddress: get(elements, "DAG"),
172
+ city: get(elements, "DAI"),
173
+ state: get(elements, "DAJ"),
174
+ postalCode: get(elements, "DAK")?.trim() ?? null,
175
+ country: get(elements, "DCG"),
176
+ documentNumber,
177
+ documentDiscriminator: get(elements, "DCF"),
178
+ sex: parseSex(get(elements, "DBC"), header.aamvaVersion),
179
+ height: get(elements, "DAU"),
180
+ weight: get(elements, "DAW"),
181
+ eyeColor: tryParseEyeColor(get(elements, "DAY")),
182
+ hairColor: tryParseHairColor(get(elements, "DAZ")),
183
+ streetAddress2: get(elements, "DAH"),
184
+ complianceType: get(elements, "DDA"),
185
+ cardRevisionDate: tryParseDate(get(elements, "DDB"), header.aamvaVersion, country),
186
+ limitedDurationDocument: parseIndicator(get(elements, "DDD")),
187
+ organDonor,
188
+ veteran: parseIndicator(get(elements, "DDL")),
189
+ firstNameTruncation: get(elements, "DDF"),
190
+ middleNameTruncation: get(elements, "DDG"),
191
+ lastNameTruncation: get(elements, "DDE"),
192
+ placeOfBirth: get(elements, "DCI"),
193
+ auditInformation: get(elements, "DCJ"),
194
+ inventoryControlNumber: get(elements, "DCK"),
195
+ lastNameAlias,
196
+ firstNameAlias,
197
+ suffixAlias: get(elements, "DBS"),
198
+ raceEthnicity: tryParseRaceEthnicity(get(elements, "DCL")),
199
+ hazmatEndorsementExpiration: tryParseDate(get(elements, "DDC"), header.aamvaVersion, country),
200
+ cdlIndicator: parseIndicator(get(elements, "DDM")),
201
+ nonDomiciledIndicator: parseIndicator(get(elements, "DDN")),
202
+ enhancedDocumentIndicator: parseIndicator(get(elements, "DDO")),
203
+ permitIndicator: parseIndicator(get(elements, "DDP")),
204
+ // Driving privilege (DCA/DCB/DCD with v1 fallbacks)
205
+ vehicleClass,
206
+ restrictionCodes,
207
+ endorsementCodes,
208
+ // Physical description alternatives
209
+ heightCm: getOrNull(elements, "DAV"),
210
+ weightKg: getOrNull(elements, "DAX"),
211
+ weightRange: tryParseWeightRange(get(elements, "DCE")),
212
+ // Standard classification descriptions
213
+ federalCommercialVehicleCodes: get(elements, "DCH"),
214
+ standardVehicleClassification: get(elements, "DCM"),
215
+ standardEndorsementCode: get(elements, "DCN"),
216
+ standardRestrictionCode: get(elements, "DCO"),
217
+ vehicleClassDescription: get(elements, "DCP"),
218
+ endorsementCodeDescription: get(elements, "DCQ"),
219
+ restrictionCodeDescription: get(elements, "DCR"),
220
+ // Name fields (v1/v2-3 legacy)
221
+ namePrefix: getOrNull(elements, "DAF"),
222
+ firstNameV2: get(elements, "DCT"),
223
+ // V1 legacy name fields
224
+ lastNameV1: getOrNull(elements, "DAB"),
225
+ nameSuffixV1: getOrNull(elements, "DAE"),
226
+ // V1 driving privilege (raw v1 values)
227
+ classificationCodeV1: getOrNull(elements, "DAR"),
228
+ restrictionCodeV1: getOrNull(elements, "DAS"),
229
+ endorsementCodeV1: getOrNull(elements, "DAT"),
230
+ // Residence address (v1)
231
+ residenceStreetAddress: get(elements, "DAL"),
232
+ residenceStreetAddress2: get(elements, "DAM"),
233
+ residenceCity: get(elements, "DAN"),
234
+ residenceJurisdictionCode: get(elements, "DAO"),
235
+ residencePostalCode: get(elements, "DAP"),
236
+ // Document/administrative
237
+ issueTimestamp: get(elements, "DBE"),
238
+ numberOfDuplicates: get(elements, "DBF"),
239
+ organDonorLegacy: getOrNull(elements, "DBH"),
240
+ nonResidentIndicator: get(elements, "DBI"),
241
+ uniqueCustomerId: getOrNull(elements, "DBJ"),
242
+ socialSecurityNumber: get(elements, "DBK"),
243
+ akaDateOfBirth: tryParseDate(get(elements, "DBL"), header.aamvaVersion, country),
244
+ akaSocialSecurityNumber: get(elements, "DBM"),
245
+ // AKA (v1 element IDs)
246
+ akaLastNameV1: getOrNull(elements, "DBO"),
247
+ akaFirstNameV1: getOrNull(elements, "DBP"),
248
+ akaMiddleName: getOrNull(elements, "DBQ"),
249
+ akaSuffixV1: getOrNull(elements, "DBR"),
250
+ // Age milestone dates
251
+ under18Until: tryParseDate(get(elements, "DDH"), header.aamvaVersion, country),
252
+ under19Until: tryParseDate(get(elements, "DDI"), header.aamvaVersion, country),
253
+ under21Until: tryParseDate(get(elements, "DDJ"), header.aamvaVersion, country),
254
+ // Raw element access
255
+ raw: { ...elements },
256
+ issuerId: header.issuerId,
257
+ aamvaVersion: header.aamvaVersion,
258
+ jurisdiction,
259
+ };
260
+ }
261
+ function parseLicense(barcodeString) {
262
+ return parseLicenseFromBarcode((0, barcode_1.parseBarcodeString)(barcodeString));
263
+ }
@@ -0,0 +1,6 @@
1
+ export interface RaceEthnicity {
2
+ code: string;
3
+ description: string;
4
+ }
5
+ export declare const RACE_ETHNICITIES: readonly RaceEthnicity[];
6
+ export declare function parseRaceEthnicity(code: string): RaceEthnicity;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RACE_ETHNICITIES = void 0;
4
+ exports.parseRaceEthnicity = parseRaceEthnicity;
5
+ exports.RACE_ETHNICITIES = [
6
+ { code: "AI", description: "Alaskan or American Indian" },
7
+ { code: "AP", description: "Asian or Pacific Islander" },
8
+ { code: "BK", description: "Black" },
9
+ { code: "H", description: "Hispanic Origin" },
10
+ { code: "O", description: "Non-hispanic" },
11
+ { code: "U", description: "Unknown" },
12
+ { code: "W", description: "White" },
13
+ ];
14
+ function parseRaceEthnicity(code) {
15
+ const found = exports.RACE_ETHNICITIES.find((r) => r.code === code);
16
+ if (!found) {
17
+ throw new Error(`Race/Ethnicity code '${code}' not found.`);
18
+ }
19
+ return found;
20
+ }
@@ -0,0 +1,8 @@
1
+ export interface WeightRange {
2
+ code: string;
3
+ description: string;
4
+ lbs: string;
5
+ kg: string;
6
+ }
7
+ export declare const WEIGHT_RANGES: readonly WeightRange[];
8
+ export declare function parseWeightRange(code: string): WeightRange;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WEIGHT_RANGES = void 0;
4
+ exports.parseWeightRange = parseWeightRange;
5
+ exports.WEIGHT_RANGES = [
6
+ { code: "0", description: "Up to 70 lbs (31 kg)", lbs: "0-70", kg: "0-31" },
7
+ { code: "1", description: "71-100 lbs (32-45 kg)", lbs: "71-100", kg: "32-45" },
8
+ { code: "2", description: "101-130 lbs (46-59 kg)", lbs: "101-130", kg: "46-59" },
9
+ { code: "3", description: "131-160 lbs (60-70 kg)", lbs: "131-160", kg: "60-70" },
10
+ { code: "4", description: "161-190 lbs (71-86 kg)", lbs: "161-190", kg: "71-86" },
11
+ { code: "5", description: "191-220 lbs (87-100 kg)", lbs: "191-220", kg: "87-100" },
12
+ { code: "6", description: "221-250 lbs (101-113 kg)", lbs: "221-250", kg: "101-113" },
13
+ { code: "7", description: "251-280 lbs (114-127 kg)", lbs: "251-280", kg: "114-127" },
14
+ { code: "8", description: "281-320 lbs (128-145 kg)", lbs: "281-320", kg: "128-145" },
15
+ { code: "9", description: "Over 320 lbs (145+ kg)", lbs: "321+", kg: "146+" },
16
+ ];
17
+ function parseWeightRange(code) {
18
+ const found = exports.WEIGHT_RANGES.find((w) => w.code === code);
19
+ if (!found) {
20
+ throw new Error(`Weight range code '${code}' not found.`);
21
+ }
22
+ return found;
23
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "aamva-decoder",
3
+ "version": "1.0.0",
4
+ "description": "Parse PDF417 barcode data from North American driver's licenses (AAMVA standard)",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "test": "vitest run",
13
+ "prepublishOnly": "npm run build && npm test"
14
+ },
15
+ "keywords": [
16
+ "aamva",
17
+ "barcode",
18
+ "driver-license",
19
+ "pdf417",
20
+ "react-native"
21
+ ],
22
+ "license": "MIT",
23
+ "devDependencies": {
24
+ "typescript": "^5.4.0",
25
+ "vitest": "^1.6.0"
26
+ }
27
+ }