rut.ts 3.4.0 → 4.0.1

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
@@ -1,135 +1,174 @@
1
1
  <div align="center">
2
2
  <img src="https://user-images.githubusercontent.com/12705403/158434864-7f13401a-b973-4267-b035-d9882cf6c545.png" alt="Rut.ts logo" width="100%">
3
3
  <h1>Rut.ts: Handle chilean RUT values with ease using TypeScript.</h1>
4
+
5
+ [![npm version](https://img.shields.io/npm/v/rut.ts?color=000&label=npm)](https://www.npmjs.com/package/rut.ts)
6
+ [![downloads](https://img.shields.io/npm/dm/rut.ts?color=000)](https://www.npmjs.com/package/rut.ts)
7
+ [![bundle size](https://deno.bundlejs.com/badge?q=rut.ts&treeshake=[*])](https://bundlejs.com/?q=rut.ts)
8
+ [![types included](https://img.shields.io/npm/types/rut.ts?color=000)](https://www.npmjs.com/package/rut.ts)
9
+ ![zero dependencies](https://img.shields.io/badge/dependencies-0-000)
10
+ [![license](https://img.shields.io/npm/l/rut.ts?color=000)](./LICENSE)
11
+
4
12
  </div>
5
13
 
6
- ## What is a RUT?
14
+ The complete, security-hardened toolkit for the Chilean **RUT** (Rol Único
15
+ Tributario): validate, format, clean, decompose and generate — with a
16
+ correctness contract you can rely on in production.
7
17
 
8
- The **RUT** (Rol Único Tributario) is the unique Chilean identification number used for:
9
- - Tax purposes
10
- - Legal identification
11
- - Government services
12
- - Banking and financial transactions
18
+ - 🪶 **Tiny & zero-dependency** tree-shakeable ESM, ships only what you import.
19
+ - 🔒 **Hardened by default** — bounded parsing, strict mode, generic errors. No ID values leak into logs or traces.
20
+ - 🧠 **Fully typed** — first-class TypeScript types, no `@types` package needed.
21
+ - 🌐 **Universal** — runs in Node, the browser, Deno and Bun. Uses Web Crypto when available.
22
+ - **Battle-tested** a differential harness guards every release against regressions.
13
23
 
14
- **Format**: `XX.XXX.XXX-Y` where:
15
- - `X` = Body (7-8 digits)
16
- - `Y` = Verifier digit (0-9 or K)
24
+ ## Installation
17
25
 
18
- **Example**: `12.345.678-5`
26
+ ```bash
27
+ npm install rut.ts
28
+ # or: bun add rut.ts · pnpm add rut.ts · yarn add rut.ts
29
+ ```
19
30
 
20
- The verifier digit is calculated using the [Modulo 11 algorithm](https://en.wikipedia.org/wiki/Rol_%C3%9Anico_Tributario) to validate the RUT's authenticity.
31
+ ## Quick start
21
32
 
22
- ---
33
+ ```typescript
34
+ import { validate, format, clean, decompose, isRutLike } from 'rut.ts'
23
35
 
24
- ## Features
36
+ // Validate — strict mode also rejects suspicious placeholder RUTs
37
+ validate('12.345.678-5') // true
38
+ validate('12.345.678-0') // false (wrong verifier)
39
+ validate('11.111.111-1', { strict: true }) // false (suspicious)
25
40
 
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.
29
- - **Incremental Formatting**: Format RUTs progressively as the user types (ideal for form inputs).
30
- - **Decomposition**: Split a RUT into its body and verifier digit.
31
- - **Generation**: Generate valid random RUT numbers.
32
- - **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.
41
+ // Format accepts compact or formatted input
42
+ format('123456785') // '12.345.678-5'
43
+ format('123456785', { dots: false }) // '12345678-5'
44
+ format('123456789', { throwOnError: false }) // null (wrong verifier)
35
45
 
36
- ## Installation
46
+ // Format progressively as the user types (great for form inputs)
47
+ format('1234', { incremental: true }) // '1.234'
48
+ format('123456785', { incremental: true }) // '12.345.678-5'
37
49
 
38
- Using [bun](https://bun.sh/):
50
+ // Clean & decompose
51
+ clean('12.345.678-5') // '123456785'
52
+ decompose('12.345.678-5') // { body: '12345678', verifier: '5' }
39
53
 
40
- $ bun add rut.ts
54
+ // Cheap shape check, no full validation
55
+ isRutLike('12.345.678-5') // true
41
56
 
42
- Using [npm](https://www.npmjs.com/):
57
+ // Safe mode everywhere — return null instead of throwing
58
+ format('abc', { throwOnError: false }) // null
59
+ ```
43
60
 
44
- $ npm install rut.ts
61
+ > 📚 Full guides and live examples: **[rut.arrowsw.com](https://rut.arrowsw.com/)**
45
62
 
46
- Using [pnpm](https://pnpm.io/):
63
+ ## Features
47
64
 
48
- $ pnpm install rut.ts
65
+ - **Validation** verifier check with bounded input parsing and an optional `strict` mode that rejects placeholder/repeated-digit RUTs.
66
+ - **Formatting** — standardized output, with or without dots.
67
+ - **Incremental formatting** — progressive formatting as the user types, ideal for form inputs.
68
+ - **Cleaning** — permissively strip extraneous characters and leading zeros.
69
+ - **Decomposition** — split a RUT into its body and verifier digit.
70
+ - **Generation** — cryptographically-backed random valid RUTs for tests (Web Crypto when available).
71
+ - **Calculate verifier** — compute the verifier digit for a given body.
72
+ - **Format detection** — cheap `isRutLike` check without full validation.
73
+ - **Safe mode** — every safe function supports `throwOnError: false` to return `null` instead of throwing.
49
74
 
75
+ <details>
76
+ <summary><strong>New to RUTs? What the format means</strong></summary>
50
77
 
51
- ## Quick Examples
78
+ <br>
52
79
 
53
- ```typescript
54
- import { validate, format, clean, isRutLike, decompose } from 'rut.ts'
80
+ The **RUT** (Rol Único Tributario) is the unique Chilean identification number
81
+ used for tax, legal identification, government services, and banking.
55
82
 
56
- // 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)
83
+ **Format**: `XX.XXX.XXX-Y`
60
84
 
61
- // Formatting
62
- format('123456785') // '12.345.678-5'
63
- format('123456785', { dots: false }) // '12345678-5'
85
+ - `X` = Body (7–8 digits)
86
+ - `Y` = Verifier digit (`0`–`9` or `K`)
64
87
 
65
- // Incremental formatting (for form inputs)
66
- format('1234', { incremental: true }) // '1.234'
67
- format('12345678', { incremental: true }) // '1.234.567-8' (8 chars = complete)
68
- format('123456785', { incremental: true }) // '12.345.678-5' (9 chars = complete)
88
+ **Example**: `12.345.678-5`
69
89
 
70
- // Cleaning
71
- clean('12.345.678-5') // '123456785'
90
+ The verifier digit is derived from the body via the
91
+ [Modulo 11 algorithm](https://en.wikipedia.org/wiki/Rol_%C3%9Anico_Tributario),
92
+ which is what makes a RUT self-validating.
72
93
 
73
- // Decomposition
74
- const { body, verifier } = decompose('12.345.678-5')
75
- console.log(body) // '12345678'
76
- console.log(verifier) // '5'
94
+ </details>
77
95
 
78
- // Format detection (without full validation)
79
- isRutLike('12.345.678-5') // true
80
- isRutLike('not-a-rut') // false
96
+ ## Security & correctness
81
97
 
82
- // Safe mode (returns null instead of throwing)
83
- clean('invalid', { throwOnError: false }) // null
84
- format('abc', { throwOnError: false }) // null
85
- ```
98
+ `rut.ts` treats RUT validation as an identity-security boundary, not just string
99
+ formatting. That posture is the point of the library:
86
100
 
87
- ## Incremental Formatting
101
+ - **`validate(input, { strict: true })` is the recommended acceptance gate** for identity-sensitive flows. It rejects malformed dot grouping, caps oversized inputs before parsing, rejects repeated-digit placeholders, and compares the verifier via Modulo 11.
102
+ - **Errors are generic** (`Invalid RUT input`) so Chilean ID values never end up echoed into logs, traces, or user-visible exceptions.
103
+ - **`clean()` is intentionally permissive** — useful for display/storage normalization, but it does _not_ prove the verifier is correct. Always `validate()` before accepting a RUT.
88
104
 
89
- The `incremental` option in `format()` allows you to format RUTs progressively as the user types. This is useful for real-time formatting in form inputs.
105
+ ### Accepted input formats (the validation contract)
90
106
 
91
- ```typescript
92
- // Example: React input handler
93
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
94
- const formatted = format(e.target.value, { incremental: true })
95
- setRut(formatted)
96
- }
97
- ```
107
+ `validate()` and `isRutLike()` accept **only** these shapes (optionally with
108
+ leading zeros and surrounding whitespace, verifier `k`/`K` case-insensitive):
98
109
 
99
- > **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.
110
+ | Shape | Example | Notes |
111
+ | ---------------- | ----------------------------- | ------------------------------- |
112
+ | Compact | `123456785` | 7–8 digit body + verifier |
113
+ | Compact + hyphen | `12345678-5` | |
114
+ | Canonical dotted | `12.345.678-5`, `1.234.567-4` | Chilean grouping from the right |
100
115
 
101
- ### When to use incremental mode
116
+ Anything else is rejected, **including non-canonical dot grouping** that older
117
+ versions accepted (`12.345678-5`, `12345.678-5`, `1.2.3.4-5`), internal spaces,
118
+ commas, and any input longer than 64 chars.
102
119
 
103
- **✅ Use incremental when:**
104
- - Formatting user input in real-time as they type
105
- - Providing immediate visual feedback in form fields
106
- - Improving UX with progressive formatting
120
+ > The 64-char limit is a **security bound, not a format rule**. A real RUT is
121
+ > ~9 significant characters, so the cap never rejects a realistic RUT — it just
122
+ > refuses to _process_ implausibly long strings, neutralizing CPU/ReDoS-style
123
+ > abuse before any parsing runs.
107
124
 
108
- **❌ Don't use incremental when:**
109
- - Formatting already complete/stored RUTs (use default `format()`)
110
- - Validating RUTs (use `validate()` instead)
111
- - Processing final form submission values
125
+ > 💡 **Migrating a dataset?** If your upstream emits RUTs in a non-canonical
126
+ > shape, normalize to one of the three accepted forms before calling
127
+ > `validate()`, or sanity-check a representative sample with
128
+ > `npm run test:differential` (writes `tests/differential-report.md`).
129
+ > `clean()` / `decompose()` stay permissive — never treat their output as
130
+ > "validated".
112
131
 
113
- ## Usage
132
+ ## Incremental formatting
114
133
 
115
- Please refer to [the documentation](https://rutts-arrowsw.vercel.app/) for more detailed examples.
134
+ `format(input, { incremental: true })` formats a RUT progressively as the user
135
+ types — ideal for real-time feedback in form fields.
116
136
 
137
+ ```typescript
138
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
139
+ setRut(format(e.target.value, { incremental: true }))
140
+ }
141
+ ```
117
142
 
118
- ## TypeScript Types
143
+ **Use it for:** real-time input formatting and visual feedback.
144
+ **Don't use it for:** validating, or formatting already-complete/stored RUTs
145
+ (use `format()` / `validate()`). Incremental output may not be a valid RUT until
146
+ the input is complete — always `validate()` the final value.
119
147
 
120
- The library exports the following types:
148
+ ## TypeScript types
121
149
 
122
150
  ```typescript
123
151
  import type { DecomposedRut, FormatOptions, SafeOptions, ValidateOptions, VerifierDigit } from 'rut.ts'
124
152
 
125
- // VerifierDigit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'K'
126
- // DecomposedRut: { body: string; verifier: string }
127
- // FormatOptions: { incremental?: boolean; dots?: boolean; throwOnError?: boolean }
128
- // ValidateOptions: { strict?: boolean }
129
- // SafeOptions: { throwOnError?: boolean }
153
+ // VerifierDigit: '0' | '1' | | '9' | 'K'
154
+ // DecomposedRut: { body: string; verifier: string }
155
+ // FormatOptions: { incremental?: boolean; dots?: boolean; throwOnError?: boolean }
156
+ // ValidateOptions:{ strict?: boolean }
157
+ // SafeOptions: { throwOnError?: boolean }
130
158
  ```
131
159
 
160
+ ## Upgrading from v3
161
+
162
+ `v4` hardens validation for production identity flows and tightens the accepted
163
+ input contract (see the table above). If you're coming from `3.x`, the
164
+ [**CHANGELOG**](./CHANGELOG.md) lists every change and how to migrate — most
165
+ codebases only need to normalize input shape before `validate()`.
132
166
 
133
167
  ## Contributing
134
168
 
135
- Contributions to this library are welcome. Please feel free to submit pull requests or create issues for bugs and feature requests.
169
+ Contributions are welcome feel free to open issues for bugs and feature
170
+ requests, or submit a pull request.
171
+
172
+ ## License
173
+
174
+ [MIT](./LICENSE) © rut.ts contributors
@@ -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.1",
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",
@@ -45,11 +46,13 @@
45
46
  "eslint-plugin-prettier": "5.5.5",
46
47
  "eslint-plugin-react": "7.37.5",
47
48
  "eslint-plugin-react-hooks": "7.0.1",
49
+ "expect-type": "1.3.0",
48
50
  "jest": "30.2.0",
49
51
  "prettier": "3.8.1",
50
52
  "terser": "5.46.0",
51
53
  "ts-jest": "29.4.6",
52
- "typescript": "5.9.3"
54
+ "typescript": "5.9.3",
55
+ "typescript-eslint": "8.53.1"
53
56
  },
54
57
  "keywords": [
55
58
  "validation",
@@ -64,7 +67,7 @@
64
67
  "deconstruct"
65
68
  ],
66
69
  "repository": {
67
- "url": "https://github.com/arrowsoftwarehq/rut.ts.git",
70
+ "url": "https://github.com/arrowsw/rut.ts.git",
68
71
  "type": "git"
69
72
  },
70
73
  "files": [
@@ -74,4 +77,4 @@
74
77
  "LICENSE",
75
78
  "README.md"
76
79
  ]
77
- }
80
+ }