The complete, security-hardened toolkit for the Chilean RUT (Rol Único Tributario): validate, format, clean, decompose and generate — with a correctness contract you can rely on in production.
- 🪶 Tiny & zero-dependency — tree-shakeable ESM, ships only what you import.
- 🔒 Hardened by default — bounded parsing, strict mode, generic errors. No ID values leak into logs or traces.
- 🧠 Fully typed — first-class TypeScript types, no
@typespackage needed. - 🌐 Universal — runs in Node, the browser, Deno and Bun. Uses Web Crypto when available.
- ✅ Battle-tested — a differential harness guards every release against regressions.
npm install rut.ts
# or: bun add rut.ts · pnpm add rut.ts · yarn add rut.tsimport { validate, format, clean, decompose, isRutLike } from 'rut.ts'
// Validate — strict mode also rejects suspicious placeholder RUTs
validate('12.345.678-5') // true
validate('12.345.678-0') // false (wrong verifier)
validate('11.111.111-1', { strict: true }) // false (suspicious)
// Format — accepts compact or formatted input
format('123456785') // '12.345.678-5'
format('123456785', { dots: false }) // '12345678-5'
format('123456789', { throwOnError: false }) // null (wrong verifier)
// Format progressively as the user types (great for form inputs)
format('1234', { incremental: true }) // '1.234'
format('123456785', { incremental: true }) // '12.345.678-5'
// Clean & decompose
clean('12.345.678-5') // '123456785'
decompose('12.345.678-5') // { body: '12345678', verifier: '5' }
// Cheap shape check, no full validation
isRutLike('12.345.678-5') // true
// Safe mode everywhere — return null instead of throwing
format('abc', { throwOnError: false }) // nullimport { isValidRut, InvalidRutError, mask, equals, generate } from 'rut.ts'
import type { Rut } from 'rut.ts'
// Type guard — narrows `unknown`/`string` to the branded `Rut`
function persist(value: string) {
if (isValidRut(value)) {
const rut: Rut = value // ✅ the type system knows it was validated
}
}
// Typed errors — branch on the class/code, never on message text
try {
mask('not-a-rut') // any safe helper throws InvalidRutError in default mode
} catch (err) {
if (err instanceof InvalidRutError) err.code // 'INVALID_RUT'
}
// Mask for safe logging, and compare across shapes
mask('12.345.678-5') // '12.***.***-5'
equals('12.345.678-5', '123456785') // true
// Generation options
generate() // '29.561.896-5' (8-digit dotted, default)
generate({ format: 'compact' }) // '233715913'
generate({ bodyLength: 7, format: 'hyphen' }) // '7788862-4'
generate({ count: 3 }) // ['…', '…', '…']📚 Full guides and live examples: rut.arrowsw.com
- Validation — verifier check with bounded input parsing and an optional
strictmode that rejects placeholder/repeated-digit RUTs. - Branded types —
isValidRut()(type guard) narrows input to a brandedRut, so "this string was validated" flows through the type system. - Typed errors —
InvalidRutError(with a stablecode) instead of message-matching. - Formatting — standardized output, with or without dots.
- Incremental formatting — progressive formatting as the user types, ideal for form inputs.
- Masking —
mask()produces12.***.***-5for safe logging/display. - Comparison —
equals()compares RUTs across different shapes. - Cleaning — permissively strip extraneous characters and leading zeros.
- Decomposition — split a RUT into its body and verifier digit.
- Generation — cryptographically-backed random valid RUTs, with
bodyLength,formatandcountoptions (Web Crypto when available). - Calculate verifier — compute the verifier digit for a given body.
- Format detection — cheap
isRutLikecheck without full validation. - Safe mode — every safe function supports
throwOnError: falseto returnnullinstead of throwing.
New to RUTs? What the format means
The RUT (Rol Único Tributario) is the unique Chilean identification number used for tax, legal identification, government services, and banking.
Format: XX.XXX.XXX-Y
X= Body (7–8 digits)Y= Verifier digit (0–9orK)
Example: 12.345.678-5
The verifier digit is derived from the body via the Modulo 11 algorithm, which is what makes a RUT self-validating.
rut.ts treats RUT validation as an identity-security boundary, not just string
formatting. That posture is the point of the library:
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.- Errors are generic (
Invalid RUT input) so Chilean ID values never end up echoed into logs, traces, or user-visible exceptions. clean()is intentionally permissive — useful for display/storage normalization, but it does not prove the verifier is correct. Alwaysvalidate()before accepting a RUT.
validate() and isRutLike() accept only these shapes (optionally with
leading zeros and surrounding whitespace, verifier k/K case-insensitive):
| Shape | Example | Notes |
|---|---|---|
| Compact | 123456785 |
7–8 digit body + verifier |
| Compact + hyphen | 12345678-5 |
|
| Canonical dotted | 12.345.678-5, 1.234.567-4 |
Chilean grouping; the - is required |
Anything else is rejected, including non-canonical dot grouping that older
versions accepted (12.345678-5, 12345.678-5, 1.2.3.4-5), the dotted shape
without its verifier hyphen (12.345.6785), internal spaces, commas, and
any input longer than 64 chars.
The 64-char limit is a security bound, not a format rule. A real RUT is ~9 significant characters, so the cap never rejects a realistic RUT — it just refuses to process implausibly long strings, neutralizing CPU/ReDoS-style abuse before any parsing runs.
💡 Migrating a dataset? If your upstream emits RUTs in a non-canonical shape, normalize to one of the three accepted forms before calling
validate(), or sanity-check a representative sample withnpm run test:differential(writestests/differential-report.md).clean()/decompose()stay permissive — never treat their output as "validated".
format(input, { incremental: true }) formats a RUT progressively as the user
types — ideal for real-time feedback in form fields.
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setRut(format(e.target.value, { incremental: true }))
}Use it for: real-time input formatting and visual feedback.
Don't use it for: validating, or formatting already-complete/stored RUTs
(use format() / validate()). Incremental output may not be a valid RUT until
the input is complete — always validate() the final value.
import type {
DecomposedRut,
FormatOptions,
GenerateOptions,
Rut,
SafeOptions,
ValidateOptions,
VerifierDigit,
} from 'rut.ts'
// VerifierDigit: '0' | '1' | … | '9' | 'K'
// DecomposedRut: { body: string; verifier: VerifierDigit }
// FormatOptions: { incremental?: boolean; dots?: boolean; throwOnError?: boolean }
// ValidateOptions:{ strict?: boolean }
// SafeOptions: { throwOnError?: boolean }
// GenerateOptions:{ bodyLength?: 7 | 8; format?: 'dotted' | 'compact' | 'hyphen'; count?: number }
// Rut: string & { /* brand */ } — a validated RUT (from isValidRut)v4 hardens validation for production identity flows and tightens the accepted
input contract (see the table above). If you're coming from 3.x, the
CHANGELOG lists every change and how to migrate — most
codebases only need to normalize input shape before validate().
Contributions are welcome — feel free to open issues for bugs and feature requests, or submit a pull request.
MIT © rut.ts contributors