-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 3e16c61
Showing
23 changed files
with
14,311 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
root = true | ||
|
||
[*] | ||
charset = utf-8 | ||
end_of_line = lf | ||
insert_final_newline = true | ||
indent_style = space | ||
indent_size = 2 | ||
trim_trailing_whitespace = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Dependencies, Caches & Artifacts | ||
dist/ | ||
node_modules/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
14.16.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
module.exports = { | ||
semi: false, | ||
singleQuote: true, | ||
parser: 'typescript', | ||
tabWidth: 2, | ||
trailingComma: 'all', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"recommendations": [ | ||
"dbaeumer.vscode-eslint", | ||
"esbenp.prettier-vscode" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"files.autoSave": "afterDelay", | ||
"[javascript]": { | ||
"editor.defaultFormatter": "esbenp.prettier-vscode" | ||
}, | ||
"[typescript]": { | ||
"editor.defaultFormatter": "esbenp.prettier-vscode" | ||
}, | ||
"editor.formatOnSave": true, | ||
"editor.tabSize": 2, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
Copyright 2021 "Andres Correa Casablanca <[email protected]>" | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy of | ||
this software and associated documentation files (the "Software"), to deal in | ||
the Software without restriction, including without limitation the rights to | ||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of | ||
the Software, and to permit persons to whom the Software is furnished to do so, | ||
subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS | ||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | ||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER | ||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | ||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
# @coderspirit/nominal | ||
|
||
This library provides some powerful types to implement nominal typing and values | ||
tainting with zero runtime overhead. This can be very useful to enforce complex | ||
pre-conditions and post-conditions, and to make your code more secure against | ||
malicious inputs. | ||
|
||
## Basic nominal types | ||
|
||
To define a new nominal type based on a previous type, we can do: | ||
|
||
```typescript | ||
import { WithTag } from '@coderspirit/nominal' | ||
|
||
type Email = WithTag<string, 'Email'> | ||
|
||
const email: Email = '[email protected]' as Email | ||
``` | ||
|
||
### Advice | ||
- Although we perform a "static cast" here, this should be done only in | ||
validation, sanitization and/or anticorruption layers. | ||
- One way to protect against other developers "forging" the type is to use | ||
symbols instead of strings as "type tags" when defining the new nominal type. | ||
|
||
## Combining nominal types | ||
|
||
`WithTag` has been implemented in a way that allows us to easily compose many | ||
nominal types for a same value: | ||
|
||
```typescript | ||
import { WithTag, WithTags } from '@coderspirit/nominal' | ||
|
||
type Integer = WithTag<number, 'Integer'> | ||
type Even = WithTag<number, 'Even'> | ||
type Positive = WithTag<number, 'Positive'> | ||
|
||
// The first way is by adding "tags" to an already "tagged" type | ||
type EvenInteger = WithTag<Integer, 'Even'> | ||
|
||
// The second way is by adding many "tags" at the same time | ||
type EvenPositive = WithTags<number, ['Even', 'Positive']> | ||
``` | ||
### Interesting properties | ||
- `WithTag` and `WithTags` are additive, commutative and idempotent. | ||
- The previous point means that we don't have to worry about the order of | ||
composition, we won't suffer typing inconsistencies because of that. | ||
### Unused type tags can be preserved across function boundaries | ||
This feature can be very useful when we need to verify many properties for the | ||
same value and we don't want to lose this information along the way as the value | ||
is passed from one function to another. | ||
```typescript | ||
function throwIfNotEven<T extends number>(v: T): WithTag<T, 'Even'> { | ||
if (v % 2 == 1) throw new Error('Not Even!') | ||
return v as WithTag<T, 'Even'> | ||
} | ||
|
||
function throwIfNotPositive<T extends number>(v: T): WithTag<T, 'Positive'> { | ||
if (v <= 0) throw new Error('Not positive!') | ||
return v as WithTag<T, 'Positive'> | ||
} | ||
|
||
const v1 = 42 | ||
|
||
// typeof v2 === WithTag<number, 'Even'> | ||
const v2 = throwIfNotEven(v1) | ||
|
||
// typeof v3 === WithTags<number, ['Even', 'Positive']> | ||
const v3 = throwIfNotPositive(v2) | ||
``` | ||
|
||
## Removing nominal types | ||
|
||
If needed, we can easily remove "tags" from our types: | ||
|
||
```typescript | ||
import { WithTag, WithoutTag } from '@coderspirit/nominal' | ||
|
||
type Email = WithTag<string, 'Email'> | ||
type NotEmail = WithoutTag<string, 'Email'> | ||
type NotAnEmailAnymore = WithoutTag<Email, 'Email'> | ||
|
||
const email: Email = '[email protected]' as Email | ||
|
||
// The type checked accepts this | ||
const untypedEmail: string = email | ||
|
||
// The type checker will error with any of the following two lines | ||
const notEmail1: NotEmail = email | ||
const notEmail2: NotAnEmailAnymore = email | ||
|
||
// NotEmail & NotAnEmailAnymore are still compatible with string | ||
const notEmail3: NotEmail = 'not an email' | ||
const notEmail4: string = notEmail3 // This is OK :) | ||
|
||
const notEmail5: NotAnEmailAnymore = 'not an email anymore' | ||
const notEmail6: string = notEmail5 // This is also OK :) | ||
``` | ||
|
||
This can be a powerful building block to implement values tainting, although we | ||
already provide an out-of-box solution for that. | ||
|
||
## Tainting values | ||
|
||
While using nominal types in the typical way is often enough, sometimes it can | ||
be handy to mark all the values coming from the "external world" as tainted (and | ||
therfore "dangerous"), independently of whether we took the time to assign them | ||
a specific nominal type or not. | ||
|
||
`@coderspirit/nominal` provides the types `Tainted<T>` and `Untainted<T>`, both | ||
of them operate recursively on `T`. | ||
|
||
```typescript | ||
import { Tainted, Untainted } from '@coderspirit/nominal' | ||
|
||
interface LoginRequest { | ||
username: string | ||
password: string | ||
} | ||
|
||
type TaintedLoginRequest = Tainted<LoginRequest> | ||
type UntaintedLoginRequest = Untainted<LoginRequest> | ||
|
||
function validateLoginRequest(req: TaintedLoginRequest): UntaintedLoginRequest { | ||
// throw Error if `req` is invalid | ||
return req as UntaintedLoginRequest | ||
} | ||
|
||
// This function accepts LoginRequest and UntaintedLoginRequest, | ||
// but not TaintedLoginRequest | ||
function login(req: UntaintedLoginRequest): void { | ||
// Do stuff | ||
} | ||
|
||
// When req is tainted, we cannot pass req.password to this function, as all | ||
// req's fields are tainted as well. | ||
function doStuffWithPassword(password: Untainted<string>): void { | ||
// Do stuff | ||
} | ||
``` | ||
|
||
While this specific use case is far from being exciting and quite simplistic, | ||
this idea can be applied to much more sensitive and convoluted scenarios. | ||
|
||
### Advice | ||
|
||
If you really want to use the tainting feature, it's better to rely on these two | ||
types, rather than trying to do it with `WithTag` and `WithoutTags`, as there | ||
are some complicated details to take into account, like nested tainting. | ||
|
||
## "Safe" values | ||
|
||
We also have a handy shortcut to combine (un)tainting with other nominal types: | ||
```typescript | ||
import { Safe, Tainted } from '@coderspirit/nominal' | ||
|
||
// Compared with WithTag, it imposes slightly stricter conditions when used in | ||
// function signatures. | ||
type Email = Safe<string, 'Email'> | ||
|
||
type InsecureString = Tainted<string> | ||
|
||
// It can be applied to "tainted types" too. | ||
type PostalCode = Safe<InsecureString, 'PostalCode'> | ||
``` | ||
## Generic tainting | ||
While it's difficult to imagine how a value might be tainted in multiple ways, | ||
it's not unheard of. This could be useful when managing sensitive information, | ||
if we need/want to statically enforce that some data won't cross certain | ||
boundaries. | ||
```typescript | ||
import { GenericTainted, GenericUntainted, GenericSafe } from '@coderspirit/nominal' | ||
|
||
type BlueTaintedNumber = GenericTainted<number, 'Blue'> | ||
type RedTaintedNumber = GenericTainted<number, 'Red'> | ||
|
||
// Double-tainted type! | ||
type BlueRedTaintedNumber = GenericTainted<BlueTaintedNumber, 'Red'> | ||
|
||
// We removed again the 'Blue' taint | ||
type OnlyBlueTaintedNumber = GenericUntainted<BlueRedTaintedNumber, 'Red'> | ||
|
||
// We removed the 'Red' taint, and added a "type tag" | ||
type SafeOnBlueZoneNumber = GenericSafe<BlueRedTaintedNumber, 'SafeOnBlue', 'Blue'> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
coverage | ||
dist | ||
node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
const currentDir = __dirname || '.' | ||
|
||
module.exports = { | ||
root: true, | ||
parser: '@typescript-eslint/parser', | ||
parserOptions: { | ||
ecmaVersion: 2020, | ||
tsConfigRootDir: __dirname, | ||
project: [ | ||
`${currentDir}/tsconfig.json`, | ||
`${currentDir}/src/__tests__/tsconfig.json`, | ||
], | ||
}, | ||
plugins: ['@typescript-eslint'], | ||
extends: [ | ||
'eslint:recommended', | ||
'plugin:@typescript-eslint/eslint-recommended', | ||
'plugin:@typescript-eslint/recommended', | ||
'plugin:@typescript-eslint/recommended-requiring-type-checking', | ||
'plugin:node/recommended-module', | ||
'prettier', | ||
], | ||
rules: { | ||
'node/no-missing-import': [ | ||
'error', | ||
{ | ||
resolvePaths: [ | ||
`${currentDir}/src`, | ||
`${currentDir}/node_modules`, | ||
`${currentDir}/node_modules/@types`, | ||
`${currentDir}/node_modules/@types/node`, | ||
], | ||
tryExtensions: ['.ts', '.d.ts'], | ||
}, | ||
], | ||
quotes: ['error', 'single', { avoidEscape: true }], | ||
'sort-imports': 'error', | ||
}, | ||
ignorePatterns: ['*.js', 'tsconfig.json'], | ||
overrides: [ | ||
{ | ||
files: ['**/__tests__/**/*.test.ts'], | ||
plugins: ['jest'], | ||
extends: ['plugin:jest/recommended', 'plugin:jest/style'], | ||
env: { | ||
jest: true, | ||
'jest/globals': true, | ||
}, | ||
rules: { | ||
'jest/no-disabled-tests': 'error', | ||
'jest/no-focused-tests': 'error', | ||
'jest/no-identical-title': 'error', | ||
'jest/consistent-test-it': 'error', | ||
'jest/valid-expect': ['error', { alwaysAwait: true }], | ||
}, | ||
}, | ||
], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
node_modules/ | ||
src/ | ||
.eslintignore | ||
.eslintrc.js | ||
.npmignore | ||
jest.config.js | ||
package-lock.json | ||
tsconfig.base.json | ||
tsconfig.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
// For a detailed explanation regarding each configuration property, visit: | ||
// https://jestjs.io/docs/configuration | ||
|
||
module.exports = { | ||
automock: false, | ||
clearMocks: true, | ||
preset: 'ts-jest', | ||
testEnvironment: 'node', | ||
testMatch: ['**/__tests__/**/*.test.ts'], | ||
testPathIgnorePatterns: [ | ||
'node_modules', | ||
'dist' | ||
] | ||
} |
Oops, something went wrong.