Releases: arrowsw/rut.ts
v4.0.1 — docs-only
Documentation-only release. No code or API changes — the published library is byte-for-byte identical to 4.0.0. If you're on 4.0.0 there is nothing to upgrade for functionality; this release exists to ship the documentation rewrite to npm.
Changed
- README rewritten as a value-first overview: trust badges, a quick-start example up front, and the security hardening framed as a strength. The
3.x → 4.xupgrade note was moved out of the hero and softened. - CHANGELOG gained a reassuring "Do I need to change anything?" summary, and the pre-
4.0.0version history (1.0.0→3.4.0) was reconstructed from git history and npm publish metadata. - Internal: version bump +
www/ignored (website maintained in a separate repository).
Full notes: CHANGELOG.md
🤖 Generated with Claude Code
v4.0.0
A major, breaking release that hardens the library for high-volume RUT/RUN identity validation in production. The Modulo 11 algorithm itself was already correct; this release hardens the input perimeter and tightens the "clean / format / validate" contract. Upgrading from 3.x requires reading the Breaking changes section below.
Why this is a major release
3.x accepted ambiguous input shapes, "repaired" RUTs with an incorrect verifier digit when formatting, echoed the raw input into error messages, and had a regex that was vulnerable to ReDoS on adversarial input. Fixing these correctly changes observable behavior for some inputs, so per SemVer this is a major version bump even though several items are security fixes.
Security
- ReDoS / unbounded input.
validate()andisRutLike()previously ran an ambiguous regex (/^0*(\d{1,3}(\.?\d{3})*)-?([\dkK])$/) before any length check. Adversarial input exhibited catastrophic backtracking ("0".repeat(16000) + "x"≈ 353 ms; growing super-linearly), andisRutLike("1".repeat(50000))returnedtrue. Inputs are now length-capped (MAX_RUT_INPUT_LENGTH = 64) before any regex work, and matched against bounded, non-ambiguous shape patterns (compact / compact-with-hyphen / canonical-dotted). Adversarial input is now rejected in well under 1 ms. The64cap is a security bound, not a format rule: a real RUT is ~9 significant chars (~12 formatted), so the cap never rejects a realistically formatted RUT — it only refuses to look at implausibly long strings. It is deliberately set well above any legitimate input yet small enough that the bounded patterns can never receive an attack string. strictbypass with uppercaseK.validate('8.888.888-K', { strict: true })incorrectly returnedtruebecause the suspicious-pattern regex only matched lowercasek. Suspicious detection now runs on the normalized, uppercased body and is case-safe.- PII in error messages.
getInvalidRutError()echoed the full RUT into the thrown message, which leaked Chilean ID values into logs, traces, and alerts. The message is now the constantInvalid RUT input.
Changed (Breaking)
-
format()no longer "repairs" an incorrect verifier digit. In non-incremental mode it now validates the verifier (Modulo 11) and returnsnull(safe mode) or throws (default). Previouslyformat('123456789')returned'12.345.678-9'despite a wrong verifier; it now returnsnull/ throws. Inputs with an invalid verifier such as'1234567K'are likewise rejected. -
validate()/isRutLike()reject non-canonical dot grouping. Only three shapes are accepted (optionally with leading zeros and surrounding whitespace; verifierk/Kis case-insensitive):- compact —
123456785 - compact + hyphen —
12345678-5 - canonical dotted —
12.345.678-5,1.234.567-4
Shapes accepted by
3.xbut now rejected:12.345678-5,12345.678-5,1.2.3.4.5.6.7.8-5, internal spaces (12 345 678 5), any input longer than 64 characters. - compact —
-
validate()/isRutLike()nowtrim()the input. Surrounding whitespace is tolerated (' 12.345.678-5 '→true). This is a relaxation but is still a behavior change. -
Generic error messages. Every thrown error is now exactly
Error: Invalid RUT input. Any code matching on the previousString "<rut>" is not valid as a RUT inputtext will no longer match. -
getInvalidRutErrorsignature changed from(rut: string) => stringto(_rut?: unknown) => stringand always returns the constant message. This helper is part of the exported API. -
Safe mode is now safe for non-string callers.
clean(),format(),calculateVerifier(),getBody(),getVerifier(), anddecompose()previously threw aTypeErroron non-string input (number,null,undefined, object). They now honorthrowOnError: returningnullin safe mode and throwing the genericInvalid RUT inputerror otherwise. -
Incremental
format()is capped at the maximum RUT length. Input is bounded to 64 chars before normalization and the significant value is capped to 9 chars.format('12345678901234', { incremental: true })changed from'1.234.567.890.123-4'to'12.345.678-9'. A trailingKis preserved only while the value still fits in 9 chars; beyond the cap the first 9 digits are kept and theKis dropped. Incremental mode is for live-typing display only — final values must still be checked withvalidate().
Fixed
generate()range bug. The previous rangeMath.floor(10000003 + Math.random() * 90000000)could emit 9-digit bodies thatcalculateVerifier()rejected (producing an occasional throw). The range is now a correct inclusive10000000–99999999.generate()could emit repeated-digit placeholders. It now skips all-same-digit bodies, so generated RUTs also passvalidate(_, { strict: true }).
Added
- CSPRNG-backed
generate(). Uses Web Crypto (crypto.getRandomValues) with unbiased rejection sampling when available, falling back toMath.random()on older runtimes. - Verifier hot-path optimization. The Modulo 11 sum is computed with a reverse
charCodeAtloop instead ofsplit('').reverse().reduce(), removing two array allocations per call. Measured throughput on the newvalidate(): ~9M validations/second (1M mixed inputs in ~110 ms). CHANGELOG.md.tests/differential.test.ts— a reproducible (seeded) differential harness (npm run test:differential) that runs the3.xand4.0.0validate()over a large stratified corpus (default 1,000,000 cases) and writestests/differential-report.mdlisting every input whose result changed, grouped by input shape. Use it to characterize impact on a real dataset before upgrading.- New regression tests: bounded/adversarial input, the
strictuppercase-Kbypass, non-string safe mode for every helper,format()verifier rejection, and the incremental capping / trailing-Kedge.
Internal
- Removed a dead length re-check in
parseRutLike(): afterisBoundedString()(which caps the raw length at 64) the post-trim()length > MAX_RUT_INPUT_LENGTHbranch was unreachable, sincetrim()can only shrink the string. Only the whitespace-only (length === 0) case remains. No behavior change.
Migration guide (3.x → 4.0.0)
- Validation gate: use
validate(input, { strict: true })as the acceptance check. Do not treat the output ofclean()/decompose()as proof of validity — they remain intentionally permissive normalizers. - Non-canonical input: if your data source emits RUTs in a shape other than the three accepted ones, normalize it first, or run
npm run test:differentialagainst a representative sample to get the exact list of values whose result changes. format()for display of arbitrary numbers: if you relied onformat()to "fix" arbitrary 8–9 digit numbers, note it now requires a correct verifier in non-incremental mode. Useincremental: truefor display-as-you-type, andvalidate()for acceptance.- Error handling: stop matching on error message text; switch to
throwOnError: falseand check fornull.
Full Changelog: https://github.com/arrowsw/rut.ts/blob/canary/CHANGELOG.md