Research
Security News
Malicious npm Packages Inject SSH Backdoors via Typosquatted Libraries
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
idonttrustlikethat
Advanced tools
This module helps validating incoming JSON, Form values, url params, localStorage values, server Environment objects, etc in a concise and type safe manner.
The focus of the lib is on small size and an easy API to add new validations.
Note: This module uses very precise Typescript types. Thus, it is mandatory to at least have the following tsconfig
/ tsc
's compiler options flag: strict
: true
.
This library exposes a validator for all primitive and object types so you should usually start from one of these then compose it with extra validations.
Here's how isoDate
is defined internally:
import { string, Err, Ok } from 'idonttrustlikethat'
const isoDate = string.and(str => {
const date = new Date(str)
return isNaN(date.getTime())
? Err(`Expected ISO date, got: ${pretty(str)}`)
: Ok(date)
})
isoDate.validate('2011-10-05T14:48:00.000Z').ok // true
This creates a new Validator that reads a string then tries to create a Date out of it.
You can also create an optional validation step that wouldn't make sense on its own:
import { string, Err, Ok, array, string } from 'idonttrustlikethat'
// This is essentially a basic filter() but with a nicer, custom error message.
const minSize = (size: number) => <T>(array: T[]) =>
array.length >= size
? Ok(array)
: Err(`Expected an array with at least ${size} items`)
const bigArray = array(string).and(minSize(100))
bigArray.validate(['1', '2']).ok // false
Note: the minSize
validator does exactly that, but for more input types.
If you need to start from any value, you can use the unknown
validator that always succeeds.
This can be used with any combination of validators except ones using recursion
.
You can get the exact type of a validator's value easily:
import { object, string, number } from 'idonttrustlikethat'
const person = object({
name: string,
age: number,
})
type Person = typeof person.T
const person: Person = {
name: 'Jon',
age: 80
}
If you say, use this library to validate a Form data, it's best to assign your error messages directly in the validator so that the proper error messages get accumulated, ready for you to display them.
import { object, string } from 'idonttrustlikethat'
const mandatoryFieldError = 'This field is mandatory'
const mandatoryString = string.withError(_ => mandatoryFieldError)
const formValidator = object({
name: mandatoryString,
})
// {ok: false, errors: [{path: 'name', message: 'This field is mandatory'}]}
const result = formValidator.validate({})
You don't! Some "similar" libraries offer this functionality but it's a pretty bad idea. It accumulates concerns inside your validation layer (you now have to pass DB connections, API tokens, etc to what should be dumb validators) and polutes the API signatures (once you go async for a tiny bit, everything now has to be async)
For instance, instead of trying to make a call to the DB to check some unicity constraint inside your validator, instead prepare the call's result before hand then pass that to a function that creates a new validator using that result, for instance:
import {string, object} from 'idonttrustlikethat'
function makeUserValidator(params: {isEmailKnown: boolean}) {
const {isEmailKnown} = params
return object({
name: string,
email: string
.withError(_ => 'The email is mandatory')
.filter(_ => !isEmailKnown)
.withError(_ => 'This email is already in use')
})
}
const isEmailKnown = await db.user.checkIfEmailIsKnown(...)
const validatedUser = makeUserValidator({isEmailKnown}).validate(body)
Here are all the values this library exposes:
import {
Err,
Ok,
array,
dictionary,
errorDebugString,
intersection,
union,
is,
literal,
unknown,
null as vnull,
number,
object,
string,
boolean,
tuple,
undefined,
} from 'idonttrustlikethat'
import {
isoDate,
recursion,
snakeCaseTransformation,
relativeUrl,
absoluteUrl,
url,
booleanFromString,
numberFromString,
intFromString,
minSize,
nonEmpty
} from 'idonttrustlikethat'
And all the types:
import {
Result,
Err,
Ok,
Validation,
Validator,
Configuration,
} from 'idonttrustlikethat'
Every validator has a validate
function which returns a Result (either a {ok: true, value}
or a {ok: false, errors}
)
Errors are accumulated.
import { object, errorDebugString } from 'idonttrustlikethat'
const myValidator = object({})
const result = myValidator.validate(myJson)
if (result.ok) {
console.log(result.value)
} else {
console.error(errorDebugString(result.errors))
}
In case of errors, errors
contains an Array of { message: string, path: string }
where message
is a debug error message for developers and path
is the path where the error occured (e.g people.0.name
)
errorDebugString
will give you a complete debug string of all errors, e.g.
At [root / c] Error validating the key. "c" is not a key of {
"a": true,
"b": true
}
At [root / c] Error validating the value. Type error: expected number but got string
import * as v from 'idonttrustlikethat'
v.unknown
v.string
v.number
v.boolean
v.null
v.undefined
v.string.validate(12).ok // false
Sometimes, a string
or a number
is not just any string or number but carries extra meaning, e.g: email
, uuid
, UserId
, KiloGram
, etc.
Tagging such a primitive as soon as it's being validated can help make the downstream code more robust and better documented.
import { string, object } from 'idonttrustlikethat'
type UserId = string & { __tag: 'UserId' } // Note: You can use any naming convention for the tag.
const userId = string.tagged<UserId>()
const user = object({
id: userId
})
If you don't use tagged types, it can lead to situations like:
const user = object({
id: string,
companyId: string
})
const user = {
id: '12345678',
companyId: '7cd3821a-553f-4d26-84f9-88776005612b'
}
function fetchCompanyDetails(companyId: string) {}
// Nothing prevents you from passing the wrong ID "type"
fetchCompanyDetails(user.id)
Using tagged types fixes all these problems while also retaining that type's usefulness as a basic string
/number
.
import { literal } from 'idonttrustlikethat'
// The only value that can ever pass this validation is the 'X' string literal
const validator = literal('X')
import { string, object, union } from 'idonttrustlikethat'
const person = object({
id: string,
prefs: object({
csvSeparator: union(',', ';', '|').optional(),
}),
})
validator.validate({
id: '123',
prefs: {},
}).ok // true
Note that if you validate an input object with extra properties compared to what the validator know, these will be dropped from the output.
This helps keeping a clean object and let us avoid dangerous situations such as:
import { string, object } from 'idonttrustlikethat'
const configValidator = object({
clusterId: string,
version: string
})
const config = {
clusterId: '123',
version: 'v191',
extraStuffFromTheServer: 100,
_metadata: true
}
// Let's imagine what could happen if this kept all non declared properties in the output.
const result = configValidator.validate(config)
if (result.ok) {
// As far as typescript is concerned, all values are string in the validated object, which let us manipulate it as such, perhaps to pass it some generic utility:
const configDictionary: Record<string, string> = result.value
// But it's a lie, some properties are still found in the object that aren't strings.
// This will throw an exception when the entire point of validating is to avoid that.
Object.values(configDictionary).forEach(str => str.padStart(2))
}
import { array, string } from 'idonttrustlikethat'
const validator = array(string)
validator.validate(['a', 'b']).ok // true
import { tuple, string, number } from 'idonttrustlikethat'
const validator = tuple(string, number)
validator.validate(['a', 1]).ok // true
import { union, string, number } from 'idonttrustlikethat'
const stringOrNumber = union(string, number)
validator.validate(10).ok // true
Unions of literal values do not have to use literal()
but can be passed the values directly:
import {union} from 'idonttrustlikethat'
const bag = union(null, 'hello', true, 33)
Although you could also use union
for your discriminated unions, discriminatedUnion
is faster and has better error messages for that special case. It will also catch common typos at the type level.
Note that discriminatedUnion
only works with object
and intersection
(of objects) validators. Also, the discriminating property must be either a literal
or union
of primitives.
import {discriminatedUnion, literal, string} from 'idonttrustlikethat'
const userSending = object({
type: literal('sending')
})
const userEditing = object({
type: literal('editing'),
currentText: string
})
const userChatAction = discriminatedUnion('type', userSending, userEditing)
import { intersection, object, string, number } from 'idonttrustlikethat'
const object1 = object({ id: string })
const object2 = object({ age: number })
const validator = intersection(object1, object2)
validator.validate({ id: '123', age: 80 }).ok // true
optional()
transforms a validator to allow undefined
values.
nullable()
transforms a validator to allow undefined
and null
values, akin to the std lib NonNullable
type.
If you must validate a T | null
that shouldn't possibly be undefined
, you can use union()
import { string } from 'idonttrustlikethat'
const validator = string.nullable()
const result = validator.validate(undefined)
result.ok && result.value // undefined
Returns a default value if the validated value was either null or undefined.
import { string } from 'idonttrustlikethat'
const validator = string.default(':(')
const result = validator.validate(undefined)
result.ok && result.value // :(
Sets a custom error message onto the validator.
The validator have decent error messages by default for developers but you will sometimes want to customize these.
Note that the first withError
encountering an error wins but a single withError
will apply to any error encountered in the chain.
import {object, string} from 'idonttrustlikethat'
const validator = object({
id: string
.withError(i => `Expected a string, got ${i}`) // This will activate if the input is not a string or is missing.
.and(nonEmpty())
.withError(_ => `The id cannot be the empty string`) // This will activate only if the id is a string but is empty.
})
A dictionary is an object where all keys and all values share a common type.
import { dictionary, string, number } from 'idonttrustlikethat'
const validator = dictionary(string, number)
validator.validate({
a: 1,
b: 2,
}).ok // true
If you need a partial dictionary, simply type your values as optional:
import { dictionary, string, number, union } from 'idonttrustlikethat'
const validator = dictionary(union('a', 'b', 'c'), number.optional())
validator.validate({
b: 1
}).ok // true
import { string } from 'idonttrustlikethat'
const validator = string.filter(str => str.length > 3).map(str => `${str}...`)
const result = validator.validate('1234')
result.ok // true
result.value // 1234...
Unlike map
which deals with a validated value and returns a new value, and
can return either a validated value or an error.
import { string, Ok, Err } from 'idonttrustlikethat'
const validator = string.and(str =>
str.length > 3 ? Ok(str) : Err(`No, that just won't do`)
)
then
allows the chaining of Validators. It can be used instead of and
if you already have the Validators ready to be reused.
// Validate that a string is a valid number (e.g, query string param)
const stringToInt = v.string.and(str => {
const result = Number.parseInt(str, 10)
if (Number.isFinite(result)) return Ok(result)
return Err('Expected an integer-like string, got: ' + str)
})
// unix time -> Date
const timestamp = v.number.and(n => {
const date = new Date(n)
if (isNaN(date.getTime())) return Err('Not a valid date')
return Ok(date)
})
const timeStampFromQueryString = stringToInt.then(timestamp)
timeStampFromQueryString.validate('1604341882') // {ok: true, value: Date(...)}
import { recursion, string, array, object } from 'idonttrustlikethat'
type Category = { name: string; categories: Category[] }
const category = recursion<Category>(self =>
object({
name: string,
categories: array(self),
})
)
Ensures an Array, Object, string, Map or Set has a minimum size. You can also use nonEmpty
.
import {dictionary, string} from 'idonttrustlikethat'
import {minSize} from 'idonttrustlikethat'
const dictionaryWithAtLeast10Items = dictionary(string, string).and(minSize(10))
import { isoDate } from 'idonttrustlikethat'
isoDate.validate('2011-10-05T14:48:00.000Z').ok // true
Validates that a string is a valid URL, and returns that string.
import { url, absoluteUrl, relativeUrl } from 'idonttrustlikethat'
absoluteUrl.validate('https://ebay.com').ok // true
Validates that a string encodes a boolean and returns the boolean.
import { booleanFromString } from 'idonttrustlikethat'
booleanFromString.validate('true').ok // true
Validates that a string encodes a number (float or integer) and returns the number.
import { numberFromString } from 'idonttrustlikethat'
numberFromString.validate('123.4').ok // true
Validates that a string encodes an integer and returns the number.
import { intFromString } from 'idonttrustlikethat'
intFromString.validate('123').ok // true
A Configuration object can be passed to modify the default behavior of the validators:
Configuration.transformObjectKeys
Transforms every keys of every objects before validating.
import {snakeCaseTransformation} from 'idonttrustlikethat'
const burger = v.object({
options: v.object({
doubleBacon: v.boolean,
}),
})
const ok = burger.validate(
{
options: {
double_bacon: true,
},
},
{ transformObjectKeys: snakeCaseTransformation }
)
FAQs
Validation for TypeScript
We found that idonttrustlikethat demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.