@zensors/sheriff v2.1.1
Validating user input is the most important way to protect your APIs. With Sheriff, you can easily describe what data should look like to avoid type confusion and other bugs.
Features
- 💪 Robust and well-tested marshaller
- 🌐 Works with express
- Full TypeScript support
- 🔢 Supports all JSON-serializable types
- 🏭 Prevents prototype pollution
- ⌨️ Allows custom validation functions
Get Started
Getting started with sheriff is as simple as installing it with npm or yarn.
npm install --save @zensors/sheriffor
yarn add @zensors/sheriffExample
import { M, marshal } from "@zensors/sheriff"
const description = M.obj({
name: M.str,
namespace: M.opt(M.str),
types: M.arr(M.union(
M.lit("TypeScript"),
M.lit("Flow"),
))
});
const input = JSON.parse(`{
"name": "sheriff",
"namespace": "@zensors",
"types": ["TypeScript"]
}`);
marshal(input, description);
console.log("input.name is now typed as string:", input.name);Usage
Sheriff is a declarative library for creating data specifications and validating them against untrusted input. Although it is primarily designed for securing APIs, it can also be used anywhere that data is validated.
Unlike most other data validation libraries, Sheriff is designed around TypeScript. As a result, when you marshal a value against a sheriff description, TypeScript will automatically know what the value's type is.
Creating Marshallers
The @zensors/sheriff package exports a value called M which contains a large number of composable marshallers. The can be combined to create more complex marshallers.
For instance, if you wanted to create a marshaller for the type
type Example =
| { kind: "person", name: string, age?: number }
| { kind: "dog", goodness: number }You could compose the marshallers like this:
const exampleMarshaller = M.union(
M.obj({ kind: M.lit("person"), name: M.str, age: M.opt(M.num) }),
M.obj({ kind: M.lit("dog"), goodness: M.num })
);Additionally, you can also create your own custom marshallers using M.custom. For instance, if we wanted to ensure that all dogs have at least a 10/10 "goodness", we could change the marshaller to be
M.custom(M.num, (goodness) => {
if (goodness < 10) {
throw new Error("They're good dogs, Brent");
}
});For a complete list of all built-in marshallers, see Api.
Marshalling
Given a marshaller, you can verify that an arbitray object matches it by using the marshal function from the @zensors/sheriff package.
The first argument of this function is the object that you want to marshal, and the second argument is the marshaller. If the object is described by the marshaller, then the function returns undefined. Otherwise, it will throw a MarshalError (also exported from @zensors/sheriff) with a description of what failed.
For TypeScript users, this function is typed as an assertion that the object inhabits the marshaller's type argument, which allows you to access the value in a type-safe manner.
API
The package exports the following values:
M: a collection of utility functions for constructing marshallersmarshal<T extends X, X = unknown>(obj: X, marshaller: Marshaller<T>, name: string = "INPUT"): asserts obj is T: the function to marshal an objectMarshalError: AnErrorthat is raised bymarshalwhen the object fails to marshal
Additionally, this package also exports the following type:
Marshaller<T>: the type of a marshaller that marshals values of typeT
Built-in Marshallers
The M namespace provides the following utilities:
M.lit(value): a marshaller accepting the literal valuevalue(primitives only)M.bool: a marshaller accepting all booleansM.num: a marshaller accepting all numbersM.str: a marshaller accepting all stringsM.opt(type): a marshaller that makestypeoptional (i.e. may beundefined)M.obj(fields): given an object whose values are marshallers, produces a marshaller of an object with that structure- Note: attempting to marshal a value with excess keys will result in a marshalling error
- Note: all fields that accept
undefinedas a value will be considered optional
- Note: attempting to marshal a value with excess keys will result in a marshalling error
- Note: all fields that accept
M.arr(type): a marshaller that makes an array out of the marshallertypeM.tup(...fields): given any number of marshallers, produces a marshaller for a tuple consisting of the types specified by those marshallers in orderM.union(...types): given any number of marshallers, returns a marshaller that accepts values of any of the constituent marshaller typesM.rec(f): constructs a recursive marshaller- To use: pass a function that takes one parameter (
self) and returns a marshaller - Note: if you are using TypeScript, you must provide a type argument to this function corresponding to the recursive type you wish to construct
- To use: pass a function that takes one parameter (
M.any: a marshaller accepting any input, resulting in the TS typeany- Consider usingM.unkinstead for better typecheckingM.unk: a marshaller accepting any input, resulting in the TS typeunknownM.custom(type, fn): extends the marshallertypewith custom logic fromfnfnshould throw an error iftypeis invalid, and returnundefinedotherwise.
M.record(type): marshals any object whose keys are strings, and whose values are described bytypeM.witness(type): adds a brand to the resulting typeM.nul: a marshaller accepting onlynullM.undef: a marshaller accepting onlyundefinedM.int: a marshaller accepting onlynumbers that are also integers
Marshal Errors
The MarshalError class has the following properties:
name:string- The name of the root object being marshalledpath:(string | number)[]- The location within the input where the error occurredinfo:string- A description of why marshalling failedrule:string- The marshal rule that failedmessage:string- A human-readable description of the failure
More Examples
Tuples
import { M, marshal, Marshaller } from "@zensors/sheriff";
export const triple = <T>(marshaller: Marshaller<T>) =>
M.tup(marshaller, marshaller, marshaller);
const data = JSON.parse(`[1, 2, 3]`);
marshal(data, triple(M.num));Binary Search Tree
import { M } from "@zensors/sheriff"
type BST = {
value: number;
left?: BST;
right?: BST;
};
export const BSTMarshaller =
M.rec<BST>((bst) =>
M.custom(
M.obj({ value: M.num, left: M.opt(bst), right: M.opt(bst) }),
({ value, left, right }) => {
if (left && left.value > value) {
throw new Error("Left value must be less than current value");
}
if (right && right.value < value) {
throw new Error("Right value must be greater than current value");
}
}
)
);Express
import express from "express";
import bodyParser from "bodyParser";
import { M, marshal } from "@zensors/sheriff";
const app = express();
app.use(bodyParser.json());
app.get("/api/upload-dog", (req, res) => {
const query = req.query as unknown;
const body = req.query as unknown;
marshal(query, M.obj({ apiKey: M.str }));
marshal(body, M.obj({
name: M.str,
age: M.int,
gender: M.union(M.lit("male"), M.lit("female"), M.lit("other")),
}));
createDog(query.apiKey, body);
res.send("Ok");
});Expedite
import { Router, marshalBody, marshalQuery } from "@zensors/expedite";
import { M } from "@zensors/sheriff";
import bodyParser from "bodyParser";
const router = (new Router())
.use(bodyParser.json())
.then(marshalQuery(M.obj({ apiKey: M.str })));
router.get("/api/upload-dog")
.then(marshalBody(M.obj({
name: M.str,
age: M.str,
gender: M.union(M.lit("male"), M.lit("female"), M.lit("other")),
})))
.return((req) => {
createDog(req.query.apiKey, req.body);
return "Ok";
})Contributions
Bug reports and feature requests can be submitted through the issues tab. Unfortunately, Sheriff is not accepting code contributions at this moment.
Credit
This project is developed and maintained by Zensors, Inc..