rut.ts 3.4.0 → 4.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 CHANGED
@@ -3,15 +3,22 @@
3
3
  <h1>Rut.ts: Handle chilean RUT values with ease using TypeScript.</h1>
4
4
  </div>
5
5
 
6
+ ![Open Bundle](https://deno.bundlejs.com/badge?q=rut.ts@4.0.0)
7
+
8
+ > **v4.0.0 is a major, breaking release** focused on production identity hardening.
9
+ > Read the [CHANGELOG](./CHANGELOG.md) before upgrading from `3.x`.
10
+
6
11
  ## What is a RUT?
7
12
 
8
13
  The **RUT** (Rol Único Tributario) is the unique Chilean identification number used for:
14
+
9
15
  - Tax purposes
10
16
  - Legal identification
11
17
  - Government services
12
18
  - Banking and financial transactions
13
19
 
14
20
  **Format**: `XX.XXX.XXX-Y` where:
21
+
15
22
  - `X` = Body (7-8 digits)
16
23
  - `Y` = Verifier digit (0-9 or K)
17
24
 
@@ -23,15 +30,15 @@ The verifier digit is calculated using the [Modulo 11 algorithm](https://en.wiki
23
30
 
24
31
  ## Features
25
32
 
26
- - **Validation**: Check if a RUT is valid, with optional strict mode to detect suspicious patterns.
27
- - **Cleaning**: Remove extraneous characters and leading zeros from RUT strings.
28
- - **Formatting**: Convert RUTs into a standardized format, with or without dots.
33
+ - **Validation**: Check if a RUT is valid with bounded input parsing, verifier validation, and optional strict mode.
34
+ - **Cleaning**: Permissively remove extraneous characters and leading zeros from RUT strings.
35
+ - **Formatting**: Convert valid RUTs into a standardized format, with or without dots.
29
36
  - **Incremental Formatting**: Format RUTs progressively as the user types (ideal for form inputs).
30
37
  - **Decomposition**: Split a RUT into its body and verifier digit.
31
- - **Generation**: Generate valid random RUT numbers.
38
+ - **Generation**: Generate valid random RUT numbers for testing, using Web Crypto when available.
32
39
  - **Calculate Verifier**: Calculate the verifier digit for a given RUT body.
33
- - **Format Detection**: Check if a string looks like a RUT format with `isRutLike`.
34
- - **Safe Mode**: All functions support `throwOnError: false` to return `null` instead of throwing errors.
40
+ - **Format Detection**: Check if a bounded string looks like a RUT format with `isRutLike`.
41
+ - **Safe Mode**: All safe functions support `throwOnError: false` to return `null` instead of throwing generic errors.
35
42
 
36
43
  ## Installation
37
44
 
@@ -47,41 +54,43 @@ Using [pnpm](https://pnpm.io/):
47
54
 
48
55
  $ pnpm install rut.ts
49
56
 
50
-
51
57
  ## Quick Examples
52
58
 
53
59
  ```typescript
54
60
  import { validate, format, clean, isRutLike, decompose } from 'rut.ts'
55
61
 
56
62
  // Validation
57
- validate('12.345.678-5') // true
58
- validate('12.345.678-0') // false (wrong verifier)
59
- validate('11.111.111-1', { strict: true }) // false (strict mode rejects suspicious RUTs)
63
+ validate('12.345.678-5') // true
64
+ validate('12.345.678-0') // false (wrong verifier)
65
+ validate('11.111.111-1', { strict: true }) // false (strict mode rejects suspicious RUTs)
66
+ validate('8.888.888-K', { strict: true }) // false (uppercase K suspicious RUT)
60
67
 
61
68
  // Formatting
62
- format('123456785') // '12.345.678-5'
69
+ format('123456785') // '12.345.678-5'
63
70
  format('123456785', { dots: false }) // '12345678-5'
71
+ format('123456789', { throwOnError: false }) // null (wrong verifier)
64
72
 
65
73
  // Incremental formatting (for form inputs)
66
- format('1234', { incremental: true }) // '1.234'
74
+ format('1234', { incremental: true }) // '1.234'
67
75
  format('12345678', { incremental: true }) // '1.234.567-8' (8 chars = complete)
68
76
  format('123456785', { incremental: true }) // '12.345.678-5' (9 chars = complete)
69
77
 
70
78
  // Cleaning
71
- clean('12.345.678-5') // '123456785'
79
+ clean('12.345.678-5') // '123456785'
80
+ // clean() normalizes shape only. Use validate() before accepting a RUT.
72
81
 
73
82
  // Decomposition
74
83
  const { body, verifier } = decompose('12.345.678-5')
75
- console.log(body) // '12345678'
76
- console.log(verifier) // '5'
84
+ console.log(body) // '12345678'
85
+ console.log(verifier) // '5'
77
86
 
78
87
  // Format detection (without full validation)
79
- isRutLike('12.345.678-5') // true
80
- isRutLike('not-a-rut') // false
88
+ isRutLike('12.345.678-5') // true
89
+ isRutLike('not-a-rut') // false
81
90
 
82
91
  // Safe mode (returns null instead of throwing)
83
- clean('invalid', { throwOnError: false }) // null
84
- format('abc', { throwOnError: false }) // null
92
+ clean('invalid', { throwOnError: false }) // null
93
+ format('abc', { throwOnError: false }) // null
85
94
  ```
86
95
 
87
96
  ## Incremental Formatting
@@ -98,22 +107,60 @@ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
98
107
 
99
108
  > **Note**: Incremental formatting will format the input progressively even for incomplete RUTs. The formatted output may not represent a valid RUT until the input is complete. Always use `validate()` to verify the final RUT before processing.
100
109
 
110
+ ## Production Validation Notes
111
+
112
+ For security-sensitive identity flows, prefer `validate(input, { strict: true })` as the acceptance gate. The validator now rejects malformed dot grouping, caps oversized inputs before parsing, rejects repeated-digit placeholders in strict mode, and compares the verifier digit using the Modulo 11 algorithm.
113
+
114
+ `clean()` remains intentionally permissive for input normalization. It is useful before display or storage, but it does not prove that the verifier digit is correct. `format()` validates the verifier digit in non-incremental mode and returns `null` in safe mode for invalid complete RUTs.
115
+
116
+ Error messages are generic (`Invalid RUT input`) so invalid Chilean ID values are not echoed into logs, traces, or user-visible exceptions.
117
+
118
+ ### Accepted input formats (the validation contract)
119
+
120
+ `validate()` and `isRutLike()` accept **only** these shapes (optionally with leading
121
+ zeros and surrounding whitespace, verifier `k`/`K` case-insensitive):
122
+
123
+ | Shape | Example | Notes |
124
+ |-------|---------|-------|
125
+ | Compact | `123456785` | 7–8 digit body + verifier |
126
+ | Compact + hyphen | `12345678-5` | |
127
+ | Canonical dotted | `12.345.678-5`, `1.234.567-4` | Chilean grouping from the right |
128
+
129
+ Anything else is rejected, **including non-canonical dot grouping** that older
130
+ versions accepted: `12.345678-5`, `12345.678-5`, `1.2.3.4.5.6.7.8-5`,
131
+ internal spaces (`12 345 678 5`), commas, and any input longer than 64 chars.
132
+
133
+ > The 64-char limit is a **security bound, not a format rule**. A real RUT is
134
+ > ~9 significant characters (~12 formatted), so the cap never rejects a
135
+ > realistically formatted RUT — it only refuses to *process* implausibly long
136
+ > strings, which neutralizes CPU/ReDoS-style abuse before any parsing runs.
137
+
138
+ > ⚠️ **Migrating a large dataset?** If your upstream emits RUTs in a non-canonical
139
+ > shape, normalize it to one of the three accepted forms **before** calling
140
+ > `validate()`, or run the differential harness against a representative sample
141
+ > first: `npm run test:differential` (see
142
+ > [`tests/differential.test.ts`](./tests/differential.test.ts); it writes
143
+ > `tests/differential-report.md`). `clean()`/`decompose()` stay permissive and
144
+ > will still parse some of those shapes — never treat their output as
145
+ > "validated".
146
+
101
147
  ### When to use incremental mode
102
148
 
103
149
  **✅ Use incremental when:**
150
+
104
151
  - Formatting user input in real-time as they type
105
152
  - Providing immediate visual feedback in form fields
106
153
  - Improving UX with progressive formatting
107
154
 
108
155
  **❌ Don't use incremental when:**
156
+
109
157
  - Formatting already complete/stored RUTs (use default `format()`)
110
158
  - Validating RUTs (use `validate()` instead)
111
159
  - Processing final form submission values
112
160
 
113
161
  ## Usage
114
162
 
115
- Please refer to [the documentation](https://rutts-arrowsw.vercel.app/) for more detailed examples.
116
-
163
+ Please refer to [the documentation](https://rut.arrowsw.com/) for more detailed examples.
117
164
 
118
165
  ## TypeScript Types
119
166
 
@@ -129,7 +176,6 @@ import type { DecomposedRut, FormatOptions, SafeOptions, ValidateOptions, Verifi
129
176
  // SafeOptions: { throwOnError?: boolean }
130
177
  ```
131
178
 
132
-
133
179
  ## Contributing
134
180
 
135
181
  Contributions to this library are welcome. Please feel free to submit pull requests or create issues for bugs and feature requests.
@@ -14,9 +14,10 @@ type ValidateOptions = {
14
14
  strict?: boolean;
15
15
  };
16
16
  type VerifierDigit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'K';
17
- export declare const getInvalidRutError: (rut: string) => string;
17
+ export declare const getInvalidRutError: (_rut?: unknown) => string;
18
18
  /**
19
19
  * Cleans the input string by removing leading zeros, non-numeric characters, and ensures the RUT is uppercased.
20
+ * This is a permissive normalization helper and does not validate the verifier digit.
20
21
  * @param {string} rut - The RUT string to clean.
21
22
  * @param {SafeOptions} [options] - Configuration options.
22
23
  * @param {boolean} [options.throwOnError=true] - If true (default), throws an error for invalid RUTs. If false, returns null.
@@ -84,10 +85,10 @@ declare function decompose(rut: string, options?: SafeOptions): DecomposedRut |
84
85
  /**
85
86
  * Checks if a string has a valid RUT format (without validating the verifier digit).
86
87
  * Useful for quick format validation in UI before full validation.
87
- * @param {string} rut - The string to check.
88
+ * @param {unknown} rut - The value to check.
88
89
  * @returns {boolean} True if the string looks like a RUT format, false otherwise.
89
90
  */
90
- declare const isRutLike: (rut: string) => boolean;
91
+ declare const isRutLike: (rut: unknown) => boolean;
91
92
  /**
92
93
  * Calculates the verifier digit for a given RUT body.
93
94
  * @param {string} rutBody - The body of the RUT for which to calculate the verifier.
@@ -138,6 +139,7 @@ declare function format(rut: string, options: FormatOptions & {
138
139
  declare function format(rut: string, options?: FormatOptions): string | null;
139
140
  /**
140
141
  * Generates a random valid RUT string.
142
+ * Uses Web Crypto when available, and falls back to Math.random in older runtimes.
141
143
  * @returns {string} A randomly generated, valid RUT string.
142
144
  */
143
145
  declare const generate: () => string;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.isRutLike=exports.generate=exports.decompose=exports.getVerifier=exports.getBody=exports.calculateVerifier=exports.format=exports.clean=exports.validate=exports.getInvalidRutError=void 0;const getInvalidRutError=r=>`String "${r}" is not valid as a RUT input`;exports.getInvalidRutError=getInvalidRutError;const withThrowOption=r=>({throwOnError:null==r||r}),MIN_RUT_LENGTH=8,MAX_RUT_LENGTH=9,cleanRaw=r=>r.replace(/^0+|[^0-9kK]+/g,"").toUpperCase(),patterns={cleaning:/^0+|[^0-9kK]+/g,rutLike:/^0*(\d{1,3}(\.?\d{3})*)-?([\dkK])$/,suspicious:/^(\d)\1?\.?(\1{3})\.?(\1{3})-?(\d|k)?$/};function clean(r,t){var e;const n=null===(e=null==t?void 0:t.throwOnError)||void 0===e||e,o=r.replace(patterns.cleaning,"").toUpperCase();if(o.length<MIN_RUT_LENGTH||o.length>MAX_RUT_LENGTH){if(n)throw new Error((0,exports.getInvalidRutError)(r));return null}if(o.includes("K")&&o.indexOf("K")!==o.length-1){if(n)throw new Error((0,exports.getInvalidRutError)(r));return null}return o}function getBody(r,t){var e;const n=clean(r,withThrowOption(null==t?void 0:t.throwOnError));return null!==(e=null==n?void 0:n.slice(0,-1))&&void 0!==e?e:null}function getVerifier(r,t){var e;const n=clean(r,withThrowOption(null==t?void 0:t.throwOnError));return null!==(e=null==n?void 0:n.slice(-1))&&void 0!==e?e:null}function decompose(r,t){const e=withThrowOption(null==t?void 0:t.throwOnError),n=getBody(r,e),o=getVerifier(r,e);return null===n||null===o?null:{body:n,verifier:o}}exports.clean=clean,exports.getBody=getBody,exports.getVerifier=getVerifier,exports.decompose=decompose;const isRutLike=r=>patterns.rutLike.test(r);exports.isRutLike=isRutLike;const isSuspicious=r=>patterns.suspicious.test(r);function calculateVerifier(r,t){const e=withThrowOption(null==t?void 0:t.throwOnError),n=cleanRaw(r);if(n.length<7||n.length>8){if(e.throwOnError)throw new Error((0,exports.getInvalidRutError)(r));return null}if(!/^\d+$/.test(n)){if(e.throwOnError)throw new Error((0,exports.getInvalidRutError)(r));return null}const o=11-n.split("").reverse().reduce(((r,t,e)=>r+Number(t)*(e%6+2)),0)%11;return 11===o?"0":10===o?"K":o.toString()}exports.calculateVerifier=calculateVerifier;const validate=(r,t)=>{if("string"!=typeof r||0===r.length)return!1;if(!isRutLike(r))return!1;if((null==t?void 0:t.strict)&&isSuspicious(r))return!1;const e=decompose(r,{throwOnError:!1});if(!e)return!1;const n=calculateVerifier(e.body,{throwOnError:!1});return!!n&&n===e.verifier};function format(r,t){var e,n,o;const i={incremental:null!==(e=null==t?void 0:t.incremental)&&void 0!==e&&e,dots:null===(n=null==t?void 0:t.dots)||void 0===n||n,throwOnError:null===(o=null==t?void 0:t.throwOnError)||void 0===o||o};if(0===r.length)return"";if(i.incremental){const t=cleanRaw(r);if(0===t.length)return"";if(t.length<MIN_RUT_LENGTH){if(!i.dots||t.length<=3)return t;let r=t.slice(-3);for(let e=3;e<t.length;e+=3){const n=t.length-3-e<0?0:t.length-3-e;r=t.slice(n,t.length-e)+"."+r}return r}let e=t.slice(-1);e=t.slice(-4,-1)+"-"+e;for(let r=4;r<t.length;r+=3){const n=t.length-3-r<0?0:t.length-3-r;e=i.dots?t.slice(n,t.length-r)+"."+e:t.slice(n,t.length-r)+e}return e}const l=clean(r,withThrowOption(i.throwOnError));if(null===l)return null;if(i.dots){let r=l.slice(-4,-1)+"-"+l.substring(l.length-1);for(let t=4;t<l.length;t+=3)r=l.slice(-3-t,-t)+"."+r;return r}return l.slice(0,-1)+"-"+l.substring(l.length-1)}exports.validate=validate,exports.format=format;const generate=()=>{const r=Math.floor(10000003+9e7*Math.random()).toString();return format(r+calculateVerifier(r))};exports.generate=generate;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.isRutLike=exports.generate=exports.validate=exports.getInvalidRutError=void 0,exports.clean=clean,exports.format=format,exports.calculateVerifier=calculateVerifier,exports.getBody=getBody,exports.getVerifier=getVerifier,exports.decompose=decompose;const getInvalidRutError=t=>"Invalid RUT input";exports.getInvalidRutError=getInvalidRutError;const withThrowOption=t=>({throwOnError:null==t||t}),MIN_RUT_LENGTH=8,MAX_RUT_LENGTH=9,MIN_BODY_LENGTH=7,MAX_BODY_LENGTH=8,MAX_RUT_INPUT_LENGTH=64,MIN_GENERATED_BODY=1e7,MAX_GENERATED_BODY=99999999,UINT32_RANGE=4294967296,patterns={compact:/^0*\d{7,8}[\dkK]$/,compactWithHyphen:/^0*\d{7,8}-[\dkK]$/,dotted:/^0*\d{1,3}\.\d{3}\.\d{3}-?[\dkK]$/,invalidRutChars:/[^0-9kK]+/g,bodySeparators:/[.\-\s]+/g,bodyDigits:/^\d+$/},fail=(t,e)=>{if(e)throw new Error((0,exports.getInvalidRutError)(t));return null},isBoundedString=t=>"string"==typeof t&&t.length>0&&t.length<=64,normalizeRutValue=t=>t.replace(patterns.invalidRutChars,"").replace(/^0+/,"").toUpperCase(),isCleanRut=t=>{if(t.length<MIN_RUT_LENGTH||t.length>9)return!1;const e=t.slice(0,-1),r=t.slice(-1);return e.length>=7&&e.length<=8&&patterns.bodyDigits.test(e)&&/^[\dK]$/.test(r)},isVerifierDigit=t=>/^[\dK]$/.test(t),parseRutLike=t=>{if(!isBoundedString(t))return null;const e=t.trim();if(0===e.length)return null;if(!(patterns.compact.test(e)||patterns.compactWithHyphen.test(e)||patterns.dotted.test(e)))return null;const r=normalizeRutValue(e);return isCleanRut(r)?{body:r.slice(0,-1),verifier:r.slice(-1)}:null},cleanRaw=t=>{const e=normalizeRutValue(t.slice(0,64)),r=e.replace(/K/g,"");return(e.endsWith("K")?`${r}K`:r).slice(0,9)},normalizeRutBody=t=>{if(!isBoundedString(t))return null;const e=t.replace(patterns.bodySeparators,"").replace(/^0+/,"");return e.length<7||e.length>8?null:patterns.bodyDigits.test(e)?e:null},VERIFIER_BY_CHECK_DIGIT={1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"K",11:"0"},calculateVerifierForBody=t=>{let e=0,r=2;for(let n=t.length-1;n>=0;n-=1)e+=(t.charCodeAt(n)-48)*r,r=7===r?2:r+1;return VERIFIER_BY_CHECK_DIGIT[11-e%11]},isSuspicious=t=>{const e=t[0];for(let r=1;r<t.length;r+=1)if(t[r]!==e)return!1;return!0},randomIntInclusive=(t,e)=>{const r=e-t+1,n=globalThis.crypto;if(null==n?void 0:n.getRandomValues){const e=Math.floor(4294967296/r)*r,l=new Uint32Array(1);let i=0;do{n.getRandomValues(l),i=l[0]}while(i>=e);return t+i%r}return t+Math.floor(Math.random()*r)};function clean(t,e){var r;const n=null===(r=null==e?void 0:e.throwOnError)||void 0===r||r;if(!isBoundedString(t))return fail(t,n);const l=normalizeRutValue(t);return isCleanRut(l)?l:fail(t,n)}function getBody(t,e){var r;const n=clean(t,withThrowOption(null==e?void 0:e.throwOnError));return null!==(r=null==n?void 0:n.slice(0,-1))&&void 0!==r?r:null}function getVerifier(t,e){const r=clean(t,withThrowOption(null==e?void 0:e.throwOnError));if(null===r)return null;const n=r.slice(-1);return isVerifierDigit(n)?n:null}function decompose(t,e){const r=withThrowOption(null==e?void 0:e.throwOnError),n=getBody(t,r),l=getVerifier(t,r);return null===n||null===l?null:{body:n,verifier:l}}const isRutLike=t=>null!==parseRutLike(t);function calculateVerifier(t,e){const r=withThrowOption(null==e?void 0:e.throwOnError),n=normalizeRutBody(t);return null===n?fail(t,r.throwOnError):calculateVerifierForBody(n)}exports.isRutLike=isRutLike;const validate=(t,e)=>{const r=parseRutLike(t);return!!r&&((!(null==e?void 0:e.strict)||!isSuspicious(r.body))&&calculateVerifierForBody(r.body)===r.verifier)};function format(t,e){var r,n,l;const i={incremental:null!==(r=null==e?void 0:e.incremental)&&void 0!==r&&r,dots:null===(n=null==e?void 0:e.dots)||void 0===n||n,throwOnError:null===(l=null==e?void 0:e.throwOnError)||void 0===l||l};if("string"!=typeof t)return fail(t,i.throwOnError);if(0===t.length)return"";if(i.incremental){const e=cleanRaw(t);if(0===e.length)return"";if(e.length<MIN_RUT_LENGTH){if(!i.dots||e.length<=3)return e;let t=e.slice(-3);for(let r=3;r<e.length;r+=3){const n=e.length-3-r<0?0:e.length-3-r;t=e.slice(n,e.length-r)+"."+t}return t}let r=e.slice(-4,-1)+"-"+e.slice(-1);for(let t=4;t<e.length;t+=3){const n=e.length-3-t<0?0:e.length-3-t;r=i.dots?e.slice(n,e.length-t)+"."+r:e.slice(n,e.length-t)+r}return r}const o=clean(t,withThrowOption(i.throwOnError));if(null===o)return null;const s=o.slice(0,-1),u=o.slice(-1);if(calculateVerifierForBody(s)!==u)return fail(t,i.throwOnError);if(i.dots){let t=o.slice(-4,-1)+"-"+o.substring(o.length-1);for(let e=4;e<o.length;e+=3)t=o.slice(-3-e,-e)+"."+t;return t}return o.slice(0,-1)+"-"+o.substring(o.length-1)}exports.validate=validate;const generate=()=>{let t=randomIntInclusive(1e7,99999999).toString();for(;isSuspicious(t);)t=randomIntInclusive(1e7,99999999).toString();return format(t+calculateVerifierForBody(t))};exports.generate=generate;
@@ -14,9 +14,10 @@ type ValidateOptions = {
14
14
  strict?: boolean;
15
15
  };
16
16
  type VerifierDigit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'K';
17
- export declare const getInvalidRutError: (rut: string) => string;
17
+ export declare const getInvalidRutError: (_rut?: unknown) => string;
18
18
  /**
19
19
  * Cleans the input string by removing leading zeros, non-numeric characters, and ensures the RUT is uppercased.
20
+ * This is a permissive normalization helper and does not validate the verifier digit.
20
21
  * @param {string} rut - The RUT string to clean.
21
22
  * @param {SafeOptions} [options] - Configuration options.
22
23
  * @param {boolean} [options.throwOnError=true] - If true (default), throws an error for invalid RUTs. If false, returns null.
@@ -84,10 +85,10 @@ declare function decompose(rut: string, options?: SafeOptions): DecomposedRut |
84
85
  /**
85
86
  * Checks if a string has a valid RUT format (without validating the verifier digit).
86
87
  * Useful for quick format validation in UI before full validation.
87
- * @param {string} rut - The string to check.
88
+ * @param {unknown} rut - The value to check.
88
89
  * @returns {boolean} True if the string looks like a RUT format, false otherwise.
89
90
  */
90
- declare const isRutLike: (rut: string) => boolean;
91
+ declare const isRutLike: (rut: unknown) => boolean;
91
92
  /**
92
93
  * Calculates the verifier digit for a given RUT body.
93
94
  * @param {string} rutBody - The body of the RUT for which to calculate the verifier.
@@ -138,6 +139,7 @@ declare function format(rut: string, options: FormatOptions & {
138
139
  declare function format(rut: string, options?: FormatOptions): string | null;
139
140
  /**
140
141
  * Generates a random valid RUT string.
142
+ * Uses Web Crypto when available, and falls back to Math.random in older runtimes.
141
143
  * @returns {string} A randomly generated, valid RUT string.
142
144
  */
143
145
  declare const generate: () => string;
@@ -1 +1 @@
1
- export const getInvalidRutError=r=>`String "${r}" is not valid as a RUT input`;const withThrowOption=r=>({throwOnError:null==r||r}),MIN_RUT_LENGTH=8,MAX_RUT_LENGTH=9,cleanRaw=r=>r.replace(/^0+|[^0-9kK]+/g,"").toUpperCase(),patterns={cleaning:/^0+|[^0-9kK]+/g,rutLike:/^0*(\d{1,3}(\.?\d{3})*)-?([\dkK])$/,suspicious:/^(\d)\1?\.?(\1{3})\.?(\1{3})-?(\d|k)?$/};function clean(r,t){var n;const e=null===(n=null==t?void 0:t.throwOnError)||void 0===n||n,l=r.replace(patterns.cleaning,"").toUpperCase();if(l.length<MIN_RUT_LENGTH||l.length>MAX_RUT_LENGTH){if(e)throw new Error(getInvalidRutError(r));return null}if(l.includes("K")&&l.indexOf("K")!==l.length-1){if(e)throw new Error(getInvalidRutError(r));return null}return l}function getBody(r,t){var n;const e=clean(r,withThrowOption(null==t?void 0:t.throwOnError));return null!==(n=null==e?void 0:e.slice(0,-1))&&void 0!==n?n:null}function getVerifier(r,t){var n;const e=clean(r,withThrowOption(null==t?void 0:t.throwOnError));return null!==(n=null==e?void 0:e.slice(-1))&&void 0!==n?n:null}function decompose(r,t){const n=withThrowOption(null==t?void 0:t.throwOnError),e=getBody(r,n),l=getVerifier(r,n);return null===e||null===l?null:{body:e,verifier:l}}const isRutLike=r=>patterns.rutLike.test(r),isSuspicious=r=>patterns.suspicious.test(r);function calculateVerifier(r,t){const n=withThrowOption(null==t?void 0:t.throwOnError),e=cleanRaw(r);if(e.length<7||e.length>8){if(n.throwOnError)throw new Error(getInvalidRutError(r));return null}if(!/^\d+$/.test(e)){if(n.throwOnError)throw new Error(getInvalidRutError(r));return null}const l=11-e.split("").reverse().reduce(((r,t,n)=>r+Number(t)*(n%6+2)),0)%11;return 11===l?"0":10===l?"K":l.toString()}const validate=(r,t)=>{if("string"!=typeof r||0===r.length)return!1;if(!isRutLike(r))return!1;if((null==t?void 0:t.strict)&&isSuspicious(r))return!1;const n=decompose(r,{throwOnError:!1});if(!n)return!1;const e=calculateVerifier(n.body,{throwOnError:!1});return!!e&&e===n.verifier};function format(r,t){var n,e,l;const o={incremental:null!==(n=null==t?void 0:t.incremental)&&void 0!==n&&n,dots:null===(e=null==t?void 0:t.dots)||void 0===e||e,throwOnError:null===(l=null==t?void 0:t.throwOnError)||void 0===l||l};if(0===r.length)return"";if(o.incremental){const t=cleanRaw(r);if(0===t.length)return"";if(t.length<MIN_RUT_LENGTH){if(!o.dots||t.length<=3)return t;let r=t.slice(-3);for(let n=3;n<t.length;n+=3){const e=t.length-3-n<0?0:t.length-3-n;r=t.slice(e,t.length-n)+"."+r}return r}let n=t.slice(-1);n=t.slice(-4,-1)+"-"+n;for(let r=4;r<t.length;r+=3){const e=t.length-3-r<0?0:t.length-3-r;n=o.dots?t.slice(e,t.length-r)+"."+n:t.slice(e,t.length-r)+n}return n}const i=clean(r,withThrowOption(o.throwOnError));if(null===i)return null;if(o.dots){let r=i.slice(-4,-1)+"-"+i.substring(i.length-1);for(let t=4;t<i.length;t+=3)r=i.slice(-3-t,-t)+"."+r;return r}return i.slice(0,-1)+"-"+i.substring(i.length-1)}const generate=()=>{const r=Math.floor(10000003+9e7*Math.random()).toString();return format(r+calculateVerifier(r))};export{validate,clean,format,calculateVerifier,getBody,getVerifier,decompose,generate,isRutLike};
1
+ export const getInvalidRutError=t=>"Invalid RUT input";const withThrowOption=t=>({throwOnError:null==t||t}),MIN_RUT_LENGTH=8,MAX_RUT_LENGTH=9,MIN_BODY_LENGTH=7,MAX_BODY_LENGTH=8,MAX_RUT_INPUT_LENGTH=64,MIN_GENERATED_BODY=1e7,MAX_GENERATED_BODY=99999999,UINT32_RANGE=4294967296,patterns={compact:/^0*\d{7,8}[\dkK]$/,compactWithHyphen:/^0*\d{7,8}-[\dkK]$/,dotted:/^0*\d{1,3}\.\d{3}\.\d{3}-?[\dkK]$/,invalidRutChars:/[^0-9kK]+/g,bodySeparators:/[.\-\s]+/g,bodyDigits:/^\d+$/},fail=(t,e)=>{if(e)throw new Error(getInvalidRutError());return null},isBoundedString=t=>"string"==typeof t&&t.length>0&&t.length<=64,normalizeRutValue=t=>t.replace(patterns.invalidRutChars,"").replace(/^0+/,"").toUpperCase(),isCleanRut=t=>{if(t.length<8||t.length>9)return!1;const e=t.slice(0,-1),r=t.slice(-1);return e.length>=7&&e.length<=8&&patterns.bodyDigits.test(e)&&/^[\dK]$/.test(r)},isVerifierDigit=t=>/^[\dK]$/.test(t),parseRutLike=t=>{if(!isBoundedString(t))return null;const e=t.trim();if(0===e.length)return null;if(!(patterns.compact.test(e)||patterns.compactWithHyphen.test(e)||patterns.dotted.test(e)))return null;const r=normalizeRutValue(e);return isCleanRut(r)?{body:r.slice(0,-1),verifier:r.slice(-1)}:null},cleanRaw=t=>{const e=normalizeRutValue(t.slice(0,64)),r=e.replace(/K/g,"");return(e.endsWith("K")?`${r}K`:r).slice(0,9)},normalizeRutBody=t=>{if(!isBoundedString(t))return null;const e=t.replace(patterns.bodySeparators,"").replace(/^0+/,"");return e.length<7||e.length>8?null:patterns.bodyDigits.test(e)?e:null},VERIFIER_BY_CHECK_DIGIT={1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"K",11:"0"},calculateVerifierForBody=t=>{let e=0,r=2;for(let n=t.length-1;n>=0;n-=1)e+=(t.charCodeAt(n)-48)*r,r=7===r?2:r+1;return VERIFIER_BY_CHECK_DIGIT[11-e%11]},isSuspicious=t=>{const e=t[0];for(let r=1;r<t.length;r+=1)if(t[r]!==e)return!1;return!0},randomIntInclusive=(t,e)=>{const r=e-t+1,n=globalThis.crypto;if(null==n?void 0:n.getRandomValues){const e=Math.floor(4294967296/r)*r,l=new Uint32Array(1);let i=0;do{n.getRandomValues(l),i=l[0]}while(i>=e);return t+i%r}return t+Math.floor(Math.random()*r)};function clean(t,e){var r;const n=null===(r=null==e?void 0:e.throwOnError)||void 0===r||r;if(!isBoundedString(t))return fail(0,n);const l=normalizeRutValue(t);return isCleanRut(l)?l:fail(0,n)}function getBody(t,e){var r;const n=clean(t,withThrowOption(null==e?void 0:e.throwOnError));return null!==(r=null==n?void 0:n.slice(0,-1))&&void 0!==r?r:null}function getVerifier(t,e){const r=clean(t,withThrowOption(null==e?void 0:e.throwOnError));if(null===r)return null;const n=r.slice(-1);return isVerifierDigit(n)?n:null}function decompose(t,e){const r=withThrowOption(null==e?void 0:e.throwOnError),n=getBody(t,r),l=getVerifier(t,r);return null===n||null===l?null:{body:n,verifier:l}}const isRutLike=t=>null!==parseRutLike(t);function calculateVerifier(t,e){const r=withThrowOption(null==e?void 0:e.throwOnError),n=normalizeRutBody(t);return null===n?fail(0,r.throwOnError):calculateVerifierForBody(n)}const validate=(t,e)=>{const r=parseRutLike(t);return!!r&&((!(null==e?void 0:e.strict)||!isSuspicious(r.body))&&calculateVerifierForBody(r.body)===r.verifier)};function format(t,e){var r,n,l;const i={incremental:null!==(r=null==e?void 0:e.incremental)&&void 0!==r&&r,dots:null===(n=null==e?void 0:e.dots)||void 0===n||n,throwOnError:null===(l=null==e?void 0:e.throwOnError)||void 0===l||l};if("string"!=typeof t)return fail(0,i.throwOnError);if(0===t.length)return"";if(i.incremental){const e=cleanRaw(t);if(0===e.length)return"";if(e.length<8){if(!i.dots||e.length<=3)return e;let t=e.slice(-3);for(let r=3;r<e.length;r+=3){const n=e.length-3-r<0?0:e.length-3-r;t=e.slice(n,e.length-r)+"."+t}return t}let r=e.slice(-4,-1)+"-"+e.slice(-1);for(let t=4;t<e.length;t+=3){const n=e.length-3-t<0?0:e.length-3-t;r=i.dots?e.slice(n,e.length-t)+"."+r:e.slice(n,e.length-t)+r}return r}const o=clean(t,withThrowOption(i.throwOnError));if(null===o)return null;const u=o.slice(0,-1),s=o.slice(-1);if(calculateVerifierForBody(u)!==s)return fail(0,i.throwOnError);if(i.dots){let t=o.slice(-4,-1)+"-"+o.substring(o.length-1);for(let e=4;e<o.length;e+=3)t=o.slice(-3-e,-e)+"."+t;return t}return o.slice(0,-1)+"-"+o.substring(o.length-1)}const generate=()=>{let t=randomIntInclusive(1e7,99999999).toString();for(;isSuspicious(t);)t=randomIntInclusive(1e7,99999999).toString();return format(t+calculateVerifierForBody(t))};export{validate,clean,format,calculateVerifier,getBody,getVerifier,decompose,generate,isRutLike};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rut.ts",
3
- "version": "3.4.0",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "description": "Handle chilean RUT values with ease.",
6
6
  "author": "arrowsw",
@@ -28,6 +28,7 @@
28
28
  "lint": "eslint \"{**/*,*}.{js,ts,jsx,tsx}\"",
29
29
  "prettier": "prettier --write \"{src,tests,example/src}/**/*.{js,ts,jsx,tsx}\"",
30
30
  "test": "jest --config jest.config.cjs",
31
+ "test:differential": "RUN_DIFFERENTIAL=1 jest --config jest.config.cjs tests/differential.test.ts",
31
32
  "prepare": "npm run build",
32
33
  "prepublishOnly": "npm run test && npm run prettier && npm run lint",
33
34
  "minify:esm": "terser dist/esm/index.js -o dist/esm/index.min.js -c -m",
@@ -49,7 +50,8 @@
49
50
  "prettier": "3.8.1",
50
51
  "terser": "5.46.0",
51
52
  "ts-jest": "29.4.6",
52
- "typescript": "5.9.3"
53
+ "typescript": "5.9.3",
54
+ "typescript-eslint": "8.53.1"
53
55
  },
54
56
  "keywords": [
55
57
  "validation",
@@ -64,7 +66,7 @@
64
66
  "deconstruct"
65
67
  ],
66
68
  "repository": {
67
- "url": "https://github.com/arrowsoftwarehq/rut.ts.git",
69
+ "url": "https://github.com/arrowsw/rut.ts.git",
68
70
  "type": "git"
69
71
  },
70
72
  "files": [
@@ -74,4 +76,4 @@
74
76
  "LICENSE",
75
77
  "README.md"
76
78
  ]
77
- }
79
+ }