diff --git a/.eslintignore b/.eslintignore index d029a0e..d1e2baf 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ node_modules/ TestResults/ .vscode +dist/ diff --git a/.eslintrc.json b/.eslintrc.json index 52d36f6..d392d55 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,17 +1,31 @@ { "root": true, - "env": { - "node": true - }, - "extends": "airbnb-base", + "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 2018, - "sourceType": "script" + "sourceType": "module", + "project": "./tsconfig.json" }, + "plugins": [ + "@typescript-eslint", + "jest", + "jsdoc" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "prettier/@typescript-eslint", + "plugin:prettier/recommended", + "plugin:jsdoc/recommended", + "plugin:jest/recommended" + ], "rules": { - "linebreak-style": "off", - "no-use-before-define": [ "error", { "functions": false } ], - "strict": [ "error", "global" ], - "valid-jsdoc": "warn" + "jsdoc/require-jsdoc": [ + "warn", + { + "publicOnly": true + } + ] } } diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c94ea9b..e1aa07a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,5 +22,11 @@ jobs: - name: Yarn install run: yarn install + - name: Transpile + run: yarn build + - name: Lint and run tests run: yarn test + + - name: Pack + run: npm pack diff --git a/.gitignore b/.gitignore index d029a0e..c699d2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ TestResults/ -.vscode +.cache/ +dist/ diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..c66b00b --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "endOfLine": "auto", + "semi": true, + "singleQuote": true +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index deb0766..20aeea8 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,6 +2,7 @@ "recommendations": [ "editorconfig.editorconfig", "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", "firsttris.vscode-jest-runner", "redhat.vscode-yaml", ] diff --git a/.vscode/settings.json b/.vscode/settings.json index eb3d18b..5b12f83 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,7 @@ { "editor.codeActionsOnSave": { "source.fixAll": true - } + }, + "editor.formatOnSave": true, + "typescript.tsdk": "./node_modules/typescript/lib" } diff --git a/jest.config.js b/jest.config.js index a3c14aa..42d5f59 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,7 @@ 'use strict'; module.exports = { + preset: 'ts-jest', testEnvironment: 'node', collectCoverage: true, coverageDirectory: 'TestResults/coverage', diff --git a/lib/jwk-store.js b/lib/jwk-store.js deleted file mode 100644 index d95b9a4..0000000 --- a/lib/jwk-store.js +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright (c) AXA Assistance France - * - * Licensed under the AXA Assistance France License (the "License"); you - * may not use this file except in compliance with the License. - * A copy of the License can be found in the LICENSE.md file distributed - * together with this file. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * JWK Store library - * @module lib/jwk-store - */ - -'use strict'; - -const jose = require('node-jose'); - -const store = Symbol('store'); -const keyRotator = Symbol('keyRotator'); - -/** - * Simplified wrapper class for [node-jose]{@link https://github.com/cisco/node-jose}'s keystore. - */ -class JWKStore { - /** - * Creates a new instance of the keystore. - */ - constructor() { - this[store] = jose.JWK.createKeyStore(); - this[keyRotator] = new KeyRotator(); - } - - /** - * Generates a new random RSA key and adds it into this keystore. - * @param {Number} [size] The size in bits of the new key. Default: 2048. - * @param {String} [kid] The key ID. If omitted, a new random 'kid' will be generated. - * @param {String} [use] The intended use of the key (e.g. 'sig', 'enc'.) Default: 'sig'. - * @returns {Promise} The promise for the generated key. - */ - async generateRSA(size, kid, use) { - const key = await this[store].generate('RSA', size, { kid, use: use || 'sig' }); - this[keyRotator].add(key); - return key; - } - - /** - * Adds a JWK key to this keystore. - * @param {JsonWebKey} jwk The JWK key to add. - * @returns {Promise} The promise for the added key. - */ - async add(jwk) { - const jwkUse = { use: 'sig', ...jwk }; - - const key = await this[store].add(jwkUse); - this[keyRotator].add(key); - return key; - } - - /** - * Adds a PEM-encoded RSA key to this keystore. - * @param {String} pem The PEM-encoded key to add. - * @param {String} [kid] The key ID. If omitted, a new random 'kid' will be generated. - * @param {String} [use] The intended use of the key (e.g. 'sig', 'enc'.) Default: 'sig'. - * @returns {Promise} The promise for the added key. - */ - async addPEM(pem, kid, use) { - const key = await this[store].add(pem, 'pem', { kid, use: use || 'sig' }); - this[keyRotator].add(key); - return key; - } - - /** - * Gets a key from the keystore in a round-robin fashion. - * If a 'kid' is provided, only keys that match will be taken into account. - * @param {String} [kid] The optional key identifier to match keys against. - * @returns {JsonWebKey} The retrieved key. - */ - get(kid) { - return this[keyRotator].next(kid); - } - - /** - * Generates a JSON representation of this keystore, which conforms - * to a JWK Set from {I-D.ietf-jose-json-web-key}. - * @param {Boolean} [isPrivate = false] `true` if the private fields - * of stored keys are to be included. - * @returns {Object} The JSON representation of this keystore. - */ - toJSON(isPrivate) { - return this[store].toJSON(isPrivate); - } -} - -function KeyRotator() { - const keys = []; - - this.add = function add(key) { - if (!keys.includes(key)) { - keys.push(key); - } - }; - - this.next = function next(kid) { - const i = findNext(kid); - - if (i === -1) { - return null; - } - - return moveToTheEnd(i); - }; - - function findNext(kid) { - if (keys.length === 0) { - return -1; - } - - if (!kid) { - return 0; - } - - return keys.findIndex((x) => x.kid === kid); - } - - function moveToTheEnd(i) { - const [key] = keys.splice(i, 1); - keys.push(key); - - return key; - } -} - -module.exports = JWKStore; diff --git a/package.json b/package.json index 013afd7..729bc90 100644 --- a/package.json +++ b/package.json @@ -19,47 +19,72 @@ }, "license": "MIT", "engines": { - "node": ">=10.0.0", + "node": "^10.13 || ^12.13", "yarn": "^1.15.2" }, "repository": { "type": "git", "url": "https://github.com/axa-group/oauth2-mock-server.git" }, - "main": "index.js", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", "bin": { - "oauth2-mock-server": "./bin/oauth2-mock-server.js" + "oauth2-mock-server": "./dist/oauth2-mock-server.js" }, "files": [ "CHANGELOG.md", + "LICENSE.md", "README.md", - "index.js", - "bin/", - "lib/" + "dist/**/*.*" ], "scripts": { + "build:clean": "rimraf ./dist", + "prebuild": "yarn build:clean", + "build": "tsc -p ./tsconfig.build.json", "cleanup:testresults": "rimraf TestResults", - "eslint": "eslint .", - "pretest": "yarn cleanup:testresults && yarn eslint", + "prelint": "tsc --noEmit", + "lint": "eslint --cache --cache-location .cache/ --ext=.js,.ts src test --max-warnings 0", + "pretest": "yarn cleanup:testresults && yarn lint", "test": "yarn jest" }, "dependencies": { + "@types/node-jose": "^1.1.5", "basic-auth": "^2.0.1", "body-parser": "^1.19.0", "cors": "^2.8.5", "express": "^4.17.1", "jsonwebtoken": "^8.5.1", + "lodash.isplainobject": "^4.0.6", "node-jose": "^2.0.0", "uuid": "^8.3.0" }, "devDependencies": { + "@types/basic-auth": "^1.1.3", + "@types/body-parser": "^1.19.0", + "@types/cors": "^2.8.7", + "@types/express": "^4.17.8", + "@types/jest": "^26.0.14", + "@types/jsonwebtoken": "^8.5.0", + "@types/lodash.isplainobject": "^4.0.6", + "@types/node": "^10.17.35", + "@types/supertest": "^2.0.10", + "@types/uuid": "^8.3.0", + "@typescript-eslint/eslint-plugin": "^4.3.0", + "@typescript-eslint/parser": "^4.3.0", "eslint": "^7.10.0", "eslint-config-airbnb-base": "^14.2.0", + "eslint-config-prettier": "^6.12.0", "eslint-plugin-import": "^2.22.1", "eslint-plugin-jest": "^24.0.2", + "eslint-plugin-jsdoc": "^30.6.3", + "eslint-plugin-prettier": "^3.1.4", "jest": "^26.4.2", "jest-junit": "^11.1.0", + "prettier": "^2.1.2", "rimraf": "^3.0.2", - "supertest": "^5.0.0" + "supertest": "^5.0.0", + "ts-jest": "^26.4.1", + "ts-node": "^9.0.0", + "typescript": "^4.0.3" } } diff --git a/index.js b/src/index.ts similarity index 72% rename from index.js rename to src/index.ts index 7329516..fcdfa78 100644 --- a/index.js +++ b/src/index.ts @@ -13,14 +13,6 @@ * limitations under the License. */ -'use strict'; - -const JWKStore = require('./lib/jwk-store'); -const OAuth2Issuer = require('./lib/oauth2-issuer'); -const OAuth2Server = require('./lib/oauth2-server'); - -module.exports = { - JWKStore, - OAuth2Issuer, - OAuth2Server, -}; +export { JWKStore } from './lib/jwk-store'; +export { OAuth2Issuer } from './lib/oauth2-issuer'; +export { OAuth2Server } from './lib/oauth2-server'; diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts new file mode 100644 index 0000000..5c7b82a --- /dev/null +++ b/src/lib/helpers.ts @@ -0,0 +1,80 @@ +/* eslint-disable jsdoc/require-jsdoc */ + +import { AssertionError } from 'assert'; +import type jwt from 'jsonwebtoken'; +import isPlainObject from 'lodash.isplainobject'; + +import type { TokenRequest } from './types'; + +export function assertIsString( + input: unknown, + errorMessage: string +): asserts input is string { + if (typeof input !== 'string') { + throw new AssertionError({ message: errorMessage }); + } +} + +const supportedAlgs = [ + 'HS256', + 'HS384', + 'HS512', + 'RS256', + 'RS384', + 'RS512', + 'ES256', + 'ES384', + 'ES512', + 'PS256', + 'PS384', + 'PS512', + 'none', +]; + +export function assertIsAlgorithm( + input: string +): asserts input is jwt.Algorithm { + if (!supportedAlgs.includes(input)) { + throw new AssertionError({ message: `Unssuported algorithm '${input}'` }); + } +} + +export function assertIsPlainObject( + obj: unknown, + errMessage: string +): asserts obj is Record { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + if (!isPlainObject(obj)) { + throw new AssertionError({ message: errMessage }); + } +} + +export function assertIsValidTokenRequest( + body: unknown +): asserts body is TokenRequest { + assertIsPlainObject(body, 'Invalid token request body'); + + if ('scope' in body) { + assertIsString(body.scope, "Invalid 'scope' type"); + } + + assertIsString(body.grant_type, "Invalid 'grant_type' type"); + + if ('code' in body) { + assertIsString(body.code, "Invalid 'code' type"); + } +} + +export function shift(arr: string[]): string { + if (arr.length === 0) { + throw new AssertionError({ message: 'Empty array' }); + } + + const val = arr.shift(); + + if (val === undefined) { + throw new AssertionError({ message: 'Empty value' }); + } + + return val; +} diff --git a/lib/http-server.js b/src/lib/http-server.ts similarity index 61% rename from lib/http-server.js rename to src/lib/http-server.ts index 35ba215..7a30b88 100644 --- a/lib/http-server.js +++ b/src/lib/http-server.ts @@ -15,67 +15,74 @@ /** * HTTP Server library + * * @module lib/http-server */ -'use strict'; - -const http = require('http'); - -const server = Symbol('server'); +import { AssertionError } from 'assert'; +import { Server, RequestListener, createServer } from 'http'; +import { AddressInfo } from 'net'; /** * Provides a restartable wrapper for http.CreateServer(). */ -class HttpServer { - /** - * @callback requestHandler - * @param {http.IncomingMessage} request The incoming message. - * @param {http.ServerResponse} response The server response. - */ +export class HttpServer { + // Can't use symbol as an indexer because of https://github.com/microsoft/TypeScript/issues/1863 + // Switching to ECMAScript private fields https://devblogs.microsoft.com/typescript/announcing-typescript-3-8-beta/#ecmascript-private-fields + #server: Server; /** * Creates a new instance of HttpServer. - * @param {requestHandler} requestListener The function that will handle the server's requests. + * + * @param {RequestListener} requestListener The function that will handle the server's requests. */ - constructor(requestListener) { - this[server] = http.createServer(requestListener); + constructor(requestListener: RequestListener) { + this.#server = createServer(requestListener); } /** * Returns a value indicating whether or not the server is listening for connections. - * @type {Boolean} + * + * @type {boolean} */ - get listening() { - return this[server].listening; + get listening(): boolean { + return this.#server.listening; } /** * Returns the bound address, family name and port where the server is listening, * or null if the server has not been started. + * * @returns {AddressInfo} The server bound address information. */ - address() { + address(): AddressInfo { if (!this.listening) { throw new Error('Server is not started.'); } - return this[server].address(); + const address = this.#server.address(); + + if (address === null || typeof address === 'string') { + throw new AssertionError({ message: 'Unexpected address type' }); + } + + return address; } /** * Starts the server. - * @param {Number} [port] Port number. If omitted, it will be assigned by the operating system. - * @param {String} [host] Host name. + * + * @param {number} [port] Port number. If omitted, it will be assigned by the operating system. + * @param {string} [host] Host name. * @returns {Promise} A promise that resolves when the server has been started. */ - async start(port, host) { + async start(port?: number, host?: string): Promise { if (this.listening) { throw new Error('Server has already been started.'); } return new Promise((resolve, reject) => { - this[server] + this.#server .listen(port, host) .on('listening', resolve) .on('error', reject); @@ -84,15 +91,16 @@ class HttpServer { /** * Stops the server. + * * @returns {Promise} Resolves when the server has been stopped. */ - async stop() { + async stop(): Promise { if (!this.listening) { throw new Error('Server is not started.'); } return new Promise((resolve, reject) => { - this[server].close((err) => { + this.#server.close((err) => { if (err) { return reject(err); } @@ -102,5 +110,3 @@ class HttpServer { }); } } - -module.exports = HttpServer; diff --git a/src/lib/jwk-store.ts b/src/lib/jwk-store.ts new file mode 100644 index 0000000..757e420 --- /dev/null +++ b/src/lib/jwk-store.ts @@ -0,0 +1,150 @@ +/** + * Copyright (c) AXA Assistance France + * + * Licensed under the AXA Assistance France License (the "License"); you + * may not use this file except in compliance with the License. + * A copy of the License can be found in the LICENSE.md file distributed + * together with this file. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * JWK Store library + * + * @module lib/jwk-store + */ + +import { JWK } from 'node-jose'; + +/** + * Simplified wrapper class for [node-jose]{@link https://github.com/cisco/node-jose}'s keystore. + */ +export class JWKStore { + #store: JWK.KeyStore; + #keyRotator: KeyRotator; + + /** + * Creates a new instance of the keystore. + */ + constructor() { + this.#store = JWK.createKeyStore(); + this.#keyRotator = new KeyRotator(); + } + + /** + * Generates a new random RSA key and adds it into this keystore. + * + * @param {number} [size] The size in bits of the new key. Default: 2048. + * @param {string} [kid] The key ID. If omitted, a new random 'kid' will be generated. + * @param {string} [use] The intended use of the key (e.g. 'sig', 'enc'.) Default: 'sig'. + * @returns {Promise} The promise for the generated key. + */ + async generateRSA( + size?: number, + kid?: string, + use = 'sig' + ): Promise { + const key = await this.#store.generate('RSA', size, { kid, use }); + this.#keyRotator.add(key); + return key; + } + + /** + * Adds a JWK key to this keystore. + * + * @param {JWK.Key} jwk The JWK key to add. + * @returns {Promise} The promise for the added key. + */ + async add(jwk: JWK.Key): Promise { + const jwkUse: JWK.Key = { ...jwk, use: 'sig' }; + + const key = await this.#store.add(jwkUse); + this.#keyRotator.add(key); + return key; + } + + /** + * Adds a PEM-encoded RSA key to this keystore. + * + * @param {string} pem The PEM-encoded key to add. + * @param {string} [kid] The key ID. If omitted, a new random 'kid' will be generated. + * @param {string} [use] The intended use of the key (e.g. 'sig', 'enc'.) Default: 'sig'. + * @returns {Promise} The promise for the added key. + */ + async addPEM(pem: string, kid?: string, use = 'sig'): Promise { + const key = await this.#store.add(pem, 'pem', { kid, use }); + this.#keyRotator.add(key); + return key; + } + + /** + * Gets a key from the keystore in a round-robin fashion. + * If a 'kid' is provided, only keys that match will be taken into account. + * + * @param {string} [kid] The optional key identifier to match keys against. + * @returns {JWK.Key | null} The retrieved key. + */ + get(kid?: string): JWK.Key | null { + return this.#keyRotator.next(kid); + } + + /** + * Generates a JSON representation of this keystore, which conforms + * to a JWK Set from {I-D.ietf-jose-json-web-key}. + * + * @param {boolean} [isPrivate = false] `true` if the private fields + * of stored keys are to be included. + * @returns {Object} The JSON representation of this keystore. + */ + toJSON(isPrivate?: boolean): Record { + return this.#store.toJSON(isPrivate) as Record; + } +} + +class KeyRotator { + #keys: JWK.Key[] = []; + + add(key: JWK.Key): void { + if (!this.#keys.includes(key)) { + this.#keys.push(key); + } + } + + next(kid?: string): JWK.Key | null { + const i = this.findNext(kid); + + if (i === -1) { + return null; + } + + return this.moveToTheEnd(i); + } + + private findNext(kid?: string): number { + if (this.#keys.length === 0) { + return -1; + } + + if (!kid) { + return 0; + } + + return this.#keys.findIndex((x) => x.kid === kid); + } + + private moveToTheEnd(i: number): JWK.Key { + // cf. https://github.com/typescript-eslint/typescript-eslint/pull/1645 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const [key] = this.#keys.splice(i, 1); + + this.#keys.push(key); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return key; + } +} diff --git a/lib/oauth2-issuer.js b/src/lib/oauth2-issuer.ts similarity index 60% rename from lib/oauth2-issuer.js rename to src/lib/oauth2-issuer.ts index f7bae6a..3d4e4f6 100644 --- a/lib/oauth2-issuer.js +++ b/src/lib/oauth2-issuer.ts @@ -15,55 +15,68 @@ /** * OAuth2 Issuer library + * * @module lib/oauth2-issuer */ -'use strict'; - -const jwt = require('jsonwebtoken'); -const { EventEmitter } = require('events'); -const JWKStore = require('./jwk-store'); +import jwt from 'jsonwebtoken'; +import { EventEmitter } from 'events'; +import { JWK } from 'node-jose'; -const keys = Symbol('keys'); +import { JWKStore } from './jwk-store'; +import { assertIsAlgorithm, assertIsString } from './helpers'; +import type { Header, Payload, scopesOrTransform } from './types'; /** * Represents an OAuth 2 issuer. */ -class OAuth2Issuer extends EventEmitter { +export class OAuth2Issuer extends EventEmitter { + /** + * Sets or returns the issuer URL. + * + * @type {string} + */ + url: string | null; + + #keys: JWKStore; + /** * Creates a new instance of HttpServer. */ constructor() { super(); - /** - * Sets or returns the issuer URL. - * @type {String} - */ this.url = null; - this[keys] = new JWKStore(); + this.#keys = new JWKStore(); } /** * Returns the key store. + * * @type {JWKStore} */ - get keys() { - return this[keys]; + get keys(): JWKStore { + return this.#keys; } /** * Builds a JWT with the provided 'kid'. - * @param {Boolean} signed A value that indicates whether or not to sign the JWT. - * @param {String} [kid] The 'kid' of the key that will be used to sign the JWT. + * + * @param {boolean} signed A value that indicates whether or not to sign the JWT. + * @param {string} [kid] The 'kid' of the key that will be used to sign the JWT. * If omitted, the next key in the round-robin will be used. - * @param {(String|Array|jwtTransform)} [scopesOrTransform] A scope, array of scopes, + * @param {scopesOrTransform} [scopesOrTransform] A scope, array of scopes, * or JWT transformation callback. - * @param {Number} [expiresIn] Time in seconds for the JWT to expire. Default: 3600 seconds. - * @returns {String} The produced JWT. + * @param {number} [expiresIn] Time in seconds for the JWT to expire. Default: 3600 seconds. + * @returns {string} The produced JWT. * @fires OAuth2Issuer#beforeSigning */ - buildToken(signed, kid, scopesOrTransform, expiresIn) { + buildToken( + signed: boolean, + kid?: string, + scopesOrTransform?: scopesOrTransform, + expiresIn = 3600 + ): string { const key = this.keys.get(kid); if (!key) { @@ -72,14 +85,16 @@ class OAuth2Issuer extends EventEmitter { const timestamp = Math.floor(Date.now() / 1000); - const header = { + const header: Header = { kid: key.kid, }; - const payload = { + assertIsString(this.url, 'Unknown issuer url'); + + const payload: Payload = { iss: this.url, iat: timestamp, - exp: timestamp + (expiresIn || 3600), + exp: timestamp + expiresIn, nbf: timestamp - 10, }; @@ -98,6 +113,7 @@ class OAuth2Issuer extends EventEmitter { /** * Before signing event. + * * @event OAuth2Issuer#beforeSigning * @param {object} token The JWT header and payload. * @param {object} token.header The JWT header. @@ -105,8 +121,8 @@ class OAuth2Issuer extends EventEmitter { */ this.emit('beforeSigning', token); - const options = { - algorithm: ((arguments.length === 0 || signed) ? getKeyAlg(key) : 'none'), + const options: jwt.SignOptions = { + algorithm: arguments.length === 0 || signed ? getKeyAlg(key) : 'none', header: token.header, }; @@ -114,30 +130,32 @@ class OAuth2Issuer extends EventEmitter { } } -function getKeyAlg(key) { +function getKeyAlg(key: JWK.Key): jwt.Algorithm { if (key.alg) { + assertIsAlgorithm(key.alg); return key.alg; } switch (key.kty) { case 'RSA': return 'RS256'; - case 'EC': - /* eslint-disable-next-line no-bitwise */ - return `ES${key.length & 0xFFF0}`; + case 'EC': { + const length = key.length & 0xfff0; + const alg = `ES${length}`; + assertIsAlgorithm(alg); + return alg; + } default: return 'HS256'; } } -function getSecret(key) { +function getSecret(key: JWK.Key): string { switch (key.kty) { case 'RSA': case 'EC': return key.toPEM(true); default: - return key.toObject(true).k; + return (key.toJSON(true) as { k: string }).k; } } - -module.exports = OAuth2Issuer; diff --git a/lib/oauth2-server.js b/src/lib/oauth2-server.ts similarity index 63% rename from lib/oauth2-server.js rename to src/lib/oauth2-server.ts index 5cbcdcf..dd833a7 100644 --- a/lib/oauth2-server.js +++ b/src/lib/oauth2-server.ts @@ -15,24 +15,26 @@ /** * OAuth2 HTTP Server library + * * @module lib/oauth2-server */ -'use strict'; - -const { URL } = require('url'); -const net = require('net'); -const HttpServer = require('./http-server'); -const OAuth2Issuer = require('./oauth2-issuer'); -const OAuth2Service = require('./oauth2-service'); +import { URL } from 'url'; +import { isIP, AddressInfo } from 'net'; +import { Server } from 'http'; -const issuer = Symbol('issuer'); -const service = Symbol('service'); +import { HttpServer } from './http-server'; +import { OAuth2Issuer } from './oauth2-issuer'; +import { OAuth2Service } from './oauth2-service'; +import { AssertionError } from 'assert'; /** * Represents an OAuth2 HTTP server. */ -class OAuth2Server extends HttpServer { +export class OAuth2Server extends HttpServer { + private _service: OAuth2Service; + private _issuer: OAuth2Issuer; + /** * Creates a new instance of OAuth2Server. */ @@ -42,69 +44,83 @@ class OAuth2Server extends HttpServer { super(serv.requestHandler); - this[issuer] = iss; - this[service] = serv; + this._issuer = iss; + this._service = serv; } /** * Returns the OAuth2Issuer instance used by the server. + * * @type {OAuth2Issuer} */ - get issuer() { - return this[issuer]; + get issuer(): OAuth2Issuer { + return this._issuer; } /** * Returns the OAuth2Service instance used by the server. + * * @type {OAuth2Service} */ - get service() { - return this[service]; + get service(): OAuth2Service { + return this._service; } /** * Returns a value indicating whether or not the server is listening for connections. - * @type {Boolean} + * + * @type {boolean} */ - get listening() { + get listening(): boolean { return super.listening; } /** * Returns the bound address, family name and port where the server is listening, * or null if the server has not been started. + * * @returns {AddressInfo} The server bound address information. */ - address() { - return super.address(); + address(): AddressInfo { + const address = super.address(); + + if (address === null || typeof address === 'string') { + throw new AssertionError({ message: 'Unexpected address type' }); + } + + return address; } /** * Starts the server. - * @param {Number} [port] Port number. If omitted, it will be assigned by the operating system. - * @param {String} [host] Host name. + * + * @param {number} [port] Port number. If omitted, it will be assigned by the operating system. + * @param {string} [host] Host name. * @returns {Promise} A promise that resolves when the server has been started. */ - async start(port, host) { - await super.start(port, host); + async start(port?: number, host?: string): Promise { + const server = await super.start(port, host); /* istanbul ignore else */ if (!this.issuer.url) { this.issuer.url = buildIssuerUrl(host, this.address().port); } + + return server; } /** * Stops the server. + * * @returns {Promise} Resolves when the server has been stopped. */ - async stop() { + async stop(): Promise { await super.stop(); - this[issuer].url = null; + this._issuer.url = null; } } -function buildIssuerUrl(host, port) { +function buildIssuerUrl(host: string | undefined, port: number) { const url = new URL(`http://localhost:${port}`); if (host && !coversLocalhost(host)) { @@ -114,8 +130,8 @@ function buildIssuerUrl(host, port) { return url.origin; } -function coversLocalhost(address) { - switch (net.isIP(address)) { +function coversLocalhost(address: string) { + switch (isIP(address)) { case 4: return address === '0.0.0.0' || address.startsWith('127.'); case 6: @@ -124,5 +140,3 @@ function coversLocalhost(address) { return false; } } - -module.exports = OAuth2Server; diff --git a/lib/oauth2-service.js b/src/lib/oauth2-service.ts similarity index 59% rename from lib/oauth2-service.js rename to src/lib/oauth2-service.ts index 2f6ca87..846990a 100644 --- a/lib/oauth2-service.js +++ b/src/lib/oauth2-service.ts @@ -15,17 +15,21 @@ /** * OAuth2 Service library + * * @module lib/oauth2-service */ -'use strict'; +import { IncomingMessage } from 'http'; +import express, { RequestHandler, Express } from 'express'; +import cors from 'cors'; +import bodyParser from 'body-parser'; +import basicAuth from 'basic-auth'; +import { EventEmitter } from 'events'; +import { v4 as uuidv4 } from 'uuid'; -const express = require('express'); -const cors = require('cors'); -const bodyParser = require('body-parser'); -const basicAuth = require('basic-auth'); -const { EventEmitter } = require('events'); -const { v4: uuidv4 } = require('uuid'); +import { OAuth2Issuer } from './oauth2-issuer'; +import { assertIsString, assertIsValidTokenRequest } from './helpers'; +import type { jwtTransform, scopesOrTransform } from './types'; const OPENID_CONFIGURATION_PATH = '/.well-known/openid-configuration'; const TOKEN_ENDPOINT_PATH = '/token'; @@ -34,96 +38,107 @@ const AUTHORIZE_PATH = '/authorize'; const USERINFO_PATH = '/userinfo'; const REVOKE_PATH = '/revoke'; -const issuer = Symbol('issuer'); -const requestHandler = Symbol('requestHandler'); - -const buildRequestHandler = Symbol('buildRequestHandler'); -const openidConfigurationHandler = Symbol('openidConfigurationHandler'); -const jwksHandler = Symbol('jwksHandler'); -const tokenHandler = Symbol('tokenHandler'); -const authorizeHandler = Symbol('authorizeHandler'); -const userInfoHandler = Symbol('userInfoHandler'); -const revokeHandler = Symbol('revokeHandler'); - -const nonce = Symbol('nonce'); - /** * Provides a request handler for an OAuth 2 server. */ -class OAuth2Service extends EventEmitter { +export class OAuth2Service extends EventEmitter { /** * Creates a new instance of OAuth2Server. + * * @param {OAuth2Issuer} oauth2Issuer The OAuth2Issuer instance * that will be offered through the service. */ - constructor(oauth2Issuer) { + + #issuer: OAuth2Issuer; + #requestHandler: Express; + #nonce: Record; + + constructor(oauth2Issuer: OAuth2Issuer) { super(); - this[issuer] = oauth2Issuer; + this.#issuer = oauth2Issuer; - this[requestHandler] = this[buildRequestHandler](); + this.#requestHandler = this.buildRequestHandler(); - this[nonce] = {}; + this.#nonce = {}; } /** * Returns the OAuth2Issuer instance bound to this service. + * * @type {OAuth2Issuer} */ - get issuer() { - return this[issuer]; + get issuer(): OAuth2Issuer { + return this.#issuer; } /** * Builds a JWT with a key in the keystore. The key will be selected in a round-robin fashion. - * @param {Boolean} signed A value that indicates whether or not to sign the JWT. - * @param {(String|Array|jwtTransform)} [scopesOrTransform] A scope, array of scopes, + * + * @param {boolean} signed A value that indicates whether or not to sign the JWT. + * @param {scopesOrTransform} [scopesOrTransform] A scope, array of scopes, * or JWT transformation callback. - * @param {Number} [expiresIn] Time in seconds for the JWT to expire. Default: 3600 seconds. - * @param {http.IncomingMessage} req The incoming HTTP request. - * @returns {String} The produced JWT. + * @param {number} [expiresIn] Time in seconds for the JWT to expire. Default: 3600 seconds. + * @param {IncomingMessage} req The incoming HTTP request. + * @returns {string} The produced JWT. * @fires OAuth2Service#beforeTokenSigning */ - buildToken(signed, scopesOrTransform, expiresIn, req) { + buildToken( + signed: boolean, + scopesOrTransform: scopesOrTransform | undefined, + expiresIn: number, + req: IncomingMessage + ): string { this.issuer.once('beforeSigning', (token) => { /** * Before token signing event. + * * @event OAuth2Service#beforeTokenSigning * @param {object} token The unsigned JWT header and payload. * @param {object} token.header The JWT header. * @param {object} token.payload The JWT payload. - * @param {http.IncomingMessage} req The incoming HTTP request. + * @param {IncomingMessage} req The incoming HTTP request. */ this.emit('beforeTokenSigning', token, req); }); - return this.issuer.buildToken(signed, null, scopesOrTransform, expiresIn); + return this.issuer.buildToken( + signed, + undefined, + scopesOrTransform, + expiresIn + ); } /** * Returns a request handler to be used as a callback for http.createServer(). + * * @type {Function} */ - get requestHandler() { - return this[requestHandler]; + get requestHandler(): Express { + return this.#requestHandler; } - [buildRequestHandler]() { + private buildRequestHandler = () => { const app = express(); app.disable('x-powered-by'); app.use(cors()); - app.get(OPENID_CONFIGURATION_PATH, this[openidConfigurationHandler].bind(this)); - app.get(JWKS_URI_PATH, this[jwksHandler].bind(this)); - app.post(TOKEN_ENDPOINT_PATH, + app.get(OPENID_CONFIGURATION_PATH, this.openidConfigurationHandler); + app.get(JWKS_URI_PATH, this.jwksHandler); + app.post( + TOKEN_ENDPOINT_PATH, bodyParser.urlencoded({ extended: false }), - this[tokenHandler].bind(this)); - app.get(AUTHORIZE_PATH, this[authorizeHandler].bind(this)); - app.get(USERINFO_PATH, this[userInfoHandler].bind(this)); - app.post(REVOKE_PATH, this[revokeHandler].bind(this)); + this.tokenHandler + ); + app.get(AUTHORIZE_PATH, this.authorizeHandler); + app.get(USERINFO_PATH, this.userInfoHandler); + app.post(REVOKE_PATH, this.revokeHandler); return app; - } + }; + + private openidConfigurationHandler: RequestHandler = (_req, res) => { + assertIsString(this.issuer.url, 'Unknown issuer url.'); - [openidConfigurationHandler](req, res) { const openidConfig = { issuer: this.issuer.url, token_endpoint: `${this.issuer.url}${TOKEN_ENDPOINT_PATH}`, @@ -132,7 +147,11 @@ class OAuth2Service extends EventEmitter { token_endpoint_auth_methods_supported: ['none'], jwks_uri: `${this.issuer.url}${JWKS_URI_PATH}`, response_types_supported: ['code'], - grant_types_supported: ['client_credentials', 'authorization_code', 'password'], + grant_types_supported: [ + 'client_credentials', + 'authorization_code', + 'password', + ], token_endpoint_auth_signing_alg_values_supported: ['RS256'], response_modes_supported: ['query'], id_token_signing_alg_values_supported: ['RS256'], @@ -140,13 +159,13 @@ class OAuth2Service extends EventEmitter { }; return res.json(openidConfig); - } + }; - [jwksHandler](req, res) { + private jwksHandler: RequestHandler = (_req, res) => { res.json(this.issuer.keys); - } + }; - [tokenHandler](req, res) { + private tokenHandler: RequestHandler = (req, res) => { const tokenTtl = 3600; res.set({ @@ -154,17 +173,21 @@ class OAuth2Service extends EventEmitter { Pragma: 'no-cache', }); - let xfn; - let { scope } = req.body; + let xfn: scopesOrTransform | undefined; + + assertIsValidTokenRequest(req.body); + const reqBody = req.body; + + let { scope } = reqBody; switch (req.body.grant_type) { case 'client_credentials': xfn = scope; break; case 'password': - xfn = (header, payload) => { + xfn = (_header, payload) => { Object.assign(payload, { - sub: req.body.username, + sub: reqBody.username, amr: ['pwd'], scope, }); @@ -172,7 +195,7 @@ class OAuth2Service extends EventEmitter { break; case 'authorization_code': scope = 'dummy'; - xfn = (header, payload) => { + xfn = (_header, payload) => { Object.assign(payload, { sub: 'johndoe', amr: ['pwd'], @@ -182,7 +205,7 @@ class OAuth2Service extends EventEmitter { break; case 'refresh_token': scope = 'dummy'; - xfn = (header, payload) => { + xfn = (_header, payload) => { Object.assign(payload, { sub: 'johndoe', amr: ['pwd'], @@ -197,7 +220,7 @@ class OAuth2Service extends EventEmitter { } const token = this.buildToken(true, xfn, tokenTtl, req); - const body = { + const body: Record = { access_token: token, token_type: 'Bearer', expires_in: tokenTtl, @@ -206,20 +229,24 @@ class OAuth2Service extends EventEmitter { if (req.body.grant_type !== 'client_credentials') { const credentials = basicAuth(req); const clientId = credentials ? credentials.name : req.body.client_id; - body.id_token = this.buildToken(true, (header, payload) => { + + const xfn: jwtTransform = (_header, payload) => { Object.assign(payload, { sub: 'johndoe', aud: clientId, }); - if (this[nonce][req.body.code]) { + if (reqBody.code !== undefined && this.#nonce[reqBody.code]) { Object.assign(payload, { - nonce: this[nonce][req.body.code], + nonce: this.#nonce[reqBody.code], }); - delete this[nonce][req.body.code]; + delete this.#nonce[reqBody.code]; } - }, tokenTtl, req); + }; + + body.id_token = this.buildToken(true, xfn, tokenTtl, req); body.refresh_token = uuidv4(); } + const tokenEndpointResponse = { body, statusCode: 200, @@ -227,37 +254,55 @@ class OAuth2Service extends EventEmitter { /** * Before token response event. + * * @event OAuth2Service#beforeResponse * @param {object} response The response body and status code. * @param {object} response.body The body of the response. - * @param {Number} response.statusCode The HTTP status code of the response. - * @param {http.IncomingMessage} req The incoming HTTP request. + * @param {number} response.statusCode The HTTP status code of the response. + * @param {IncomingMessage} req The incoming HTTP request. */ this.emit('beforeResponse', tokenEndpointResponse, req); - return res.status(tokenEndpointResponse.statusCode).json(tokenEndpointResponse.body); - } + return res + .status(tokenEndpointResponse.statusCode) + .json(tokenEndpointResponse.body); + }; - [authorizeHandler](req, res) { + private authorizeHandler: RequestHandler = (req, res) => { const { scope, state } = req.query; const responseType = req.query.response_type; const redirectUri = req.query.redirect_uri; const code = uuidv4(); + let queryNonce: string | undefined; + + if ('nonce' in req.query) { + assertIsString(req.query.nonce, 'Invalid nonce type'); + queryNonce = req.query.nonce; + } + + assertIsString(redirectUri, 'Invalid redirectUri type'); + assertIsString(scope, 'Invalid scope type'); + assertIsString(state, 'Invalid state type'); + let targetRedirection; if (responseType === 'code') { - if (req.query.nonce) { - this[nonce][code] = req.query.nonce; + if (queryNonce !== undefined) { + this.#nonce[code] = queryNonce; } - targetRedirection = `${redirectUri}?code=${encodeURIComponent(code)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(state)}`; + targetRedirection = `${redirectUri}?code=${encodeURIComponent( + code + )}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(state)}`; } else { - targetRedirection = `${redirectUri}?error=unsupported_response_type&error_description=The+authorization+server+does+not+support+obtaining+an+access+token+using+this+response_type.&state=${encodeURIComponent(state)}`; + targetRedirection = `${redirectUri}?error=unsupported_response_type&error_description=The+authorization+server+does+not+support+obtaining+an+access+token+using+this+response_type.&state=${encodeURIComponent( + state + )}`; } res.redirect(targetRedirection); - } + }; - [userInfoHandler](req, res) { + private userInfoHandler: RequestHandler = (req, res) => { const userInfoResponse = { body: { sub: 'johndoe', @@ -267,18 +312,19 @@ class OAuth2Service extends EventEmitter { /** * Before user info event. + * * @event OAuth2Service#beforeUserinfo * @param {object} response The response body and status code. * @param {object} response.body The body of the response. - * @param {Number} response.statusCode The HTTP status code of the response. - * @param {http.IncomingMessage} req The incoming HTTP request. + * @param {number} response.statusCode The HTTP status code of the response. + * @param {IncomingMessage} req The incoming HTTP request. */ this.emit('beforeUserinfo', userInfoResponse, req); res.status(userInfoResponse.statusCode).json(userInfoResponse.body); - } + }; - [revokeHandler](req, res) { + private revokeHandler: RequestHandler = (req, res) => { const revokeResponse = { body: null, statusCode: 200, @@ -286,15 +332,15 @@ class OAuth2Service extends EventEmitter { /** * Before revoke event. + * * @event OAuth2Service#beforeRevoke * @param {object} response The response body and status code. * @param {object} response.body The body of the response. - * @param {Number} response.statusCode The HTTP status code of the response. - * @param {http.IncomingMessage} req The incoming HTTP request. + * @param {number} response.statusCode The HTTP status code of the response. + * @param {IncomingMessage} req The incoming HTTP request. */ this.emit('beforeRevoke', revokeResponse, req); return res.status(revokeResponse.statusCode).json(revokeResponse.body); - } + }; } -module.exports = OAuth2Service; diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..79ce223 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,36 @@ +import type { JWK } from 'node-jose'; + +export interface TokenRequest { + scope?: string; + grant_type: string; + username?: unknown; + client_id?: unknown; + code?: string; +} + +export interface Options { + host?: string; + port: number; + keys: JWK.Key[]; + saveJWK: boolean; + savePEM: boolean; +} + +export interface Header { + kid: string; + [key: string]: unknown; +} + +export interface Payload { + iss: string; + iat: number; + exp: number; + nbf: number; + [key: string]: unknown; +} + +export type scopesOrTransform = string | string[] | jwtTransform; + +export interface jwtTransform { + (header: Header, payload: Payload): void; +} diff --git a/bin/oauth2-mock-server.js b/src/oauth2-mock-server.ts similarity index 73% rename from bin/oauth2-mock-server.js rename to src/oauth2-mock-server.ts index 0ee7bc2..c8c5394 100644 --- a/bin/oauth2-mock-server.js +++ b/src/oauth2-mock-server.ts @@ -15,31 +15,32 @@ * limitations under the License. */ -'use strict'; +import fs from 'fs'; +import { JWK } from 'node-jose'; +import path from 'path'; -const fs = require('fs'); -const path = require('path'); -const { OAuth2Server } = require('..'); +import { OAuth2Server } from './index'; +import { assertIsString, shift } from './lib/helpers'; +import type { Options } from './lib/types'; /* eslint no-console: off */ -const defaultOptions = { - host: undefined, +const defaultOptions: Options = { port: 8080, keys: [], saveJWK: false, savePEM: false, }; -module.exports = cli(...process.argv.slice(2)); +module.exports = cli(process.argv.slice(2)); -function cli(...args) { +async function cli(args: string[]): Promise { let options; try { - options = parseCliArgs(args); + options = await parseCliArgs(args); } catch (err) { - console.error(err.message); + console.error(err instanceof Error ? err.message : err); process.exitCode = 1; return Promise.reject(err); } @@ -52,11 +53,11 @@ function cli(...args) { return startServer(options); } -function parseCliArgs(args) { +async function parseCliArgs(args: string[]): Promise { const opts = { ...defaultOptions }; while (args.length > 0) { - const arg = args.shift(); + const arg = shift(args); switch (arg) { case '-h': @@ -64,16 +65,16 @@ function parseCliArgs(args) { showHelp(); return null; case '-a': - opts.host = args.shift(); + opts.host = shift(args); break; case '-p': - opts.port = parsePort(args.shift()); + opts.port = parsePort(shift(args)); break; case '--jwk': - opts.keys.push(parseJWK(args.shift())); + opts.keys.push(await parseJWK(shift(args))); break; case '--pem': - opts.keys.push(parsePEM(args.shift())); + opts.keys.push(await parsePEM(shift(args))); break; case '--save-jwk': opts.saveJWK = true; @@ -90,7 +91,7 @@ function parseCliArgs(args) { } function showHelp() { - const scriptName = path.basename(__filename, '.js'); + const scriptName = path.basename(__filename, '.ts'); console.log(`Usage: ${scriptName} [options] ${scriptName} -a localhost -p 8080 @@ -115,7 +116,7 @@ will be generated. This key can then be saved to disk with the --save-jwk or --save-pem options for later reuse.`); } -function parsePort(portStr) { +function parsePort(portStr: string) { const port = parseInt(portStr, 10); if (Number.isNaN(port) || port < 0 || port > 65535) { @@ -125,19 +126,19 @@ function parsePort(portStr) { return port; } -function parseJWK(filename) { +async function parseJWK(filename: string): Promise { const jwkStr = fs.readFileSync(filename, 'utf8'); - return JSON.parse(jwkStr); + return await JWK.asKey(jwkStr); } -function parsePEM(filename) { - return { +async function parsePEM(filename: string): Promise { + const pem = fs.readFileSync(filename, 'utf8'); + return await JWK.asKey(pem, 'pem', { kid: path.parse(filename).name, - pem: fs.readFileSync(filename, 'utf8'), - }; + }); } -function saveJWK(keys) { +function saveJWK(keys: JWK.Key[]) { keys.forEach((key) => { const filename = `${key.kid}.json`; fs.writeFileSync(filename, JSON.stringify(key.toJSON(true), null, 2)); @@ -145,7 +146,7 @@ function saveJWK(keys) { }); } -function savePEM(keys) { +function savePEM(keys: JWK.Key[]) { keys.forEach((key) => { const filename = `${key.kid}.pem`; fs.writeFileSync(filename, key.toPEM(true)); @@ -153,19 +154,16 @@ function savePEM(keys) { }); } -async function startServer(opts) { +async function startServer(opts: Options) { const server = new OAuth2Server(); - await Promise.all(opts.keys.map(async (key) => { - let jwk; - if (key.pem) { - jwk = await server.issuer.keys.addPEM(key.pem, key.kid); - } else { - jwk = await server.issuer.keys.add(key); - } + await Promise.all( + opts.keys.map(async (key) => { + const jwk = await server.issuer.keys.add(key); - console.log(`Added key with kid "${jwk.kid}"`); - })); + console.log(`Added key with kid "${jwk.kid}"`); + }) + ); if (opts.keys.length === 0) { const jwk = await server.issuer.keys.generateRSA(1024); @@ -188,8 +186,10 @@ async function startServer(opts) { const hostname = addr.family === 'IPv6' ? `[${addr.address}]` : addr.address; console.log(`OAuth 2 server listening on http://${hostname}:${addr.port}`); + assertIsString(server.issuer.url, 'Empty host'); console.log(`OAuth 2 issuer is ${server.issuer.url}`); + // eslint-disable-next-line @typescript-eslint/no-misused-promises process.once('SIGINT', async () => { await server.stop(); console.log('OAuth 2 server has been stopped.'); diff --git a/test/.eslintrc.json b/test/.eslintrc.json index faf3a05..fb7de40 100644 --- a/test/.eslintrc.json +++ b/test/.eslintrc.json @@ -1,9 +1,8 @@ { - "env": { - "jest/globals": true - }, - "plugins": [ - "jest" - ], - "extends": "plugin:jest/recommended" + "rules": { + "prettier/prettier": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "jsdoc/require-jsdoc": "off" + } } diff --git a/test/cli.test.js b/test/cli.test.ts similarity index 77% rename from test/cli.test.js rename to test/cli.test.ts index 720dad7..a021a54 100644 --- a/test/cli.test.js +++ b/test/cli.test.ts @@ -1,8 +1,6 @@ -'use strict'; +import { exec } from './lib/child-script'; -const { exec } = require('./lib/child-script'); - -const cliPath = require.resolve('../bin/oauth2-mock-server.js'); +const cliPath = require.resolve('../src/oauth2-mock-server'); describe('CLI', () => { beforeEach(() => { @@ -55,8 +53,8 @@ describe('CLI', () => { ['not-a-number'], ['-1'], ['65536'], - ])('should not allow invalid port number \'%s\'', async () => { - const res = await executeCli('-p', 'not-a-number'); + ])('should not allow invalid port number \'%s\'', async (port) => { + const res = await executeCli('-p', port); expect(res).toEqual(errorResponse('Invalid port number.')); }); @@ -68,7 +66,8 @@ describe('CLI', () => { expect(res.stdout).toMatch(/^Added key with kid "test-ec-key"$/m); expect(res.stdout).toMatch(/^Added key with kid "test-oct-key"$/m); - const { keys } = res.result.issuer; + expect(res.result).not.toBeNull(); + const { keys } = res.result!.issuer; expect(keys.get('test-rsa-key')).not.toBeNull(); expect(keys.get('test-ec-key')).not.toBeNull(); @@ -81,23 +80,28 @@ describe('CLI', () => { expect(res.stdout).toMatch(/^Added key with kid "test-rsa-key"$/m); expect(res.stdout).toMatch(/^Added key with kid "test-ec-key"$/m); - const { keys } = res.result.issuer; + expect(res.result).not.toBeNull(); + const { keys } = res.result!.issuer; expect(keys.get('test-rsa-key')).not.toBeNull(); expect(keys.get('test-ec-key')).not.toBeNull(); }); it('should allow exporting JSON-formatted keys', async () => { - const fs = require('fs'); /* eslint-disable-line global-require */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires + const fs = require('fs'); const wfn = jest.spyOn(fs, 'writeFileSync').mockImplementation(); const res = await executeCli('--save-jwk', '-p', '0'); - const key = res.result.issuer.keys.get(); + expect(res.result).not.toBeNull(); + const key = res.result!.issuer.keys.get(); + + expect(key).not.toBeNull(); expect(key).toHaveProperty('kid'); expect(wfn).toHaveBeenCalledWith( - `${key.kid}.json`, + `${key!.kid}.json`, expect.stringMatching(/^{(.|\n)+}$/), ); @@ -108,16 +112,19 @@ describe('CLI', () => { }); it('should allow exporting PEM-encoded keys', async () => { - const fs = require('fs'); /* eslint-disable-line global-require */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires + const fs = require('fs'); const wfn = jest.spyOn(fs, 'writeFileSync').mockImplementation(); const res = await executeCli('--save-pem', '-p', '0'); - const key = res.result.issuer.keys.get(); + expect(res.result).not.toBeNull(); + const key = res.result!.issuer.keys.get(); + expect(key).not.toBeNull(); expect(key).toHaveProperty('kid'); expect(wfn).toHaveBeenCalledWith( - `${key.kid}.pem`, + `${key!.kid}.pem`, expect.stringMatching(/^-----BEGIN RSA PRIVATE KEY-----\r\n(.|\r\n)+\r\n-----END RSA PRIVATE KEY-----\r\n$/), ); @@ -128,8 +135,8 @@ describe('CLI', () => { }); }); -async function executeCli(...args) { - const res = await exec(cliPath, ...args); +async function executeCli(...args: string[]) { + const res = await exec(cliPath, args); if (res.result) { await res.result.stop(); @@ -138,9 +145,10 @@ async function executeCli(...args) { return res; } -function errorResponse(message) { +function errorResponse(message: string) { return { err: expect.any(Error), + result: null, exitCode: 1, stdout: '', stderr: `${message}\n`, diff --git a/test/http-server.test.js b/test/http-server.test.ts similarity index 91% rename from test/http-server.test.js rename to test/http-server.test.ts index a3d01e2..f1922a1 100644 --- a/test/http-server.test.js +++ b/test/http-server.test.ts @@ -1,7 +1,6 @@ -'use strict'; - -const request = require('supertest'); -const HttpServer = require('../lib/http-server'); +import { RequestListener } from 'http'; +import request from 'supertest'; +import { HttpServer } from '../src/lib/http-server'; describe('HTTP Server', () => { it('should be able to start and stop the server', async () => { @@ -62,7 +61,7 @@ describe('HTTP Server', () => { }); }); -function dummyHandler(req, res) { +const dummyHandler: RequestListener = (_req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); res.end('{ "value": "Dummy response" }'); diff --git a/test/jwk-store.test.js b/test/jwk-store.test.ts similarity index 77% rename from test/jwk-store.test.js rename to test/jwk-store.test.ts index 36f8d9c..d6eaedf 100644 --- a/test/jwk-store.test.js +++ b/test/jwk-store.test.ts @@ -1,7 +1,5 @@ -'use strict'; - -const JWKStore = require('../lib/jwk-store'); -const testKeys = require('./keys'); +import { JWKStore } from '../src/lib/jwk-store'; +import * as testKeys from './keys'; describe('JWK Store', () => { it('should be able to generate a new RSA key', async () => { @@ -17,9 +15,9 @@ describe('JWK Store', () => { }); it.each([ - ['RSA', testKeys.get('test-rsa-key.json'), 512], - ['EC', testKeys.get('test-ec-key.json'), 256], - ['oct', testKeys.get('test-oct-key.json'), 512], + ['RSA', testKeys.getParsed('test-rsa-key.json'), 512], + ['EC', testKeys.getParsed('test-ec-key.json'), 256], + ['oct', testKeys.getParsed('test-oct-key.json'), 512], ])('should be able to add a JWK \'%s\' key to the store', async (keyType, testKey, expectedLength) => { const store = new JWKStore(); const key = await store.add(testKey); @@ -37,7 +35,7 @@ describe('JWK Store', () => { ['EC', testKeys.get('test-ec-key.pem'), 256], ])('should be able to add a PEM-encoded \'%s\' key to the store', async (keyType, testPEMKey, expectedLength) => { const store = new JWKStore(); - const key = await store.addPEM(testPEMKey, null, 'sig'); + const key = await store.addPEM(testPEMKey); expect(key).toMatchObject({ length: expectedLength, @@ -69,7 +67,7 @@ describe('JWK Store', () => { expect(stored3).toBeNull(); }); - it('should return null when trying to retrieve a key from an empty store', async () => { + it('should return null when trying to retrieve a key from an empty store', () => { const store = new JWKStore(); const res1 = store.get(); @@ -86,11 +84,22 @@ describe('JWK Store', () => { await store.generateRSA(512, 'key-two'); const jwks = store.toJSON(); + expect(jwks).toHaveProperty("keys"); + expect(jwks.keys).toBeInstanceOf(Array) + + const keys = jwks.keys as unknown[]; + expect(keys).toHaveLength(3); + + for (const key of keys) { + expect(key).toBeInstanceOf(Object); + expect(key).toHaveProperty("kid"); + expect(typeof (key as Record).kid).toBe("string") + } - expect(jwks.keys).toHaveLength(3); - expect(jwks.keys.map((key) => key.kid).sort()).toEqual(['key-one', 'key-two', 'key-two']); + const keysWithKid = keys as { kid: string }[] + expect(keysWithKid.map((key) => key.kid).sort()).toEqual(['key-one', 'key-two', 'key-two']); - jwks.keys.forEach((jwk) => { + keysWithKid.forEach((jwk) => { expect(store.get(jwk.kid)).not.toBeNull(); ['e', 'n'].forEach((prop) => { diff --git a/test/keys/index.js b/test/keys/index.js deleted file mode 100644 index 38e0f87..0000000 --- a/test/keys/index.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); - -function get(filename) { - const filePath = path.join(__dirname, filename); - let key = fs.readFileSync(filePath, 'utf8'); - - if (filename.endsWith('.json')) { - key = JSON.parse(key); - } - - return key; -} - -module.exports = { - get, -}; diff --git a/test/keys/index.ts b/test/keys/index.ts new file mode 100644 index 0000000..90fb187 --- /dev/null +++ b/test/keys/index.ts @@ -0,0 +1,16 @@ +import fs from 'fs'; +import type { JWK } from 'node-jose'; +import path from 'path'; + +export function get(filename: string): string { + const filePath = path.join(__dirname, filename); + const key = fs.readFileSync(filePath, 'utf8'); + + return key; +} + +export function getParsed(filename: string): JWK.Key { + const key = get(filename) + + return JSON.parse(key) as JWK.Key; +} diff --git a/test/lib/child-script.js b/test/lib/child-script.js deleted file mode 100644 index 4e5ac98..0000000 --- a/test/lib/child-script.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -const util = require('util'); - -/* eslint no-console: off */ - -async function exec(scriptPath, ...args) { - const argv = process.argv; /* eslint-disable-line prefer-destructuring */ - process.argv = [argv[0], scriptPath, ...args]; - - const log = new ConsoleOutHook('log'); - const error = new ConsoleOutHook('error'); - - const res = {}; - - try { - /* eslint-disable-next-line global-require, import/no-dynamic-require */ - res.result = await Promise.resolve(require(scriptPath)); - } catch (err) { - res.err = err; - } finally { - log.mockRestore(); - error.mockRestore(); - res.exitCode = process.exitCode; - process.exitCode = undefined; - } - - res.stdout = log.output; - res.stderr = error.output; - - return res; -} - -function ConsoleOutHook(method) { - this.output = ''; - - const old = console[method]; - console[method] = (format, ...param) => { - this.output += util.format(`${format}\n`, ...param); - }; - - this.mockClear = function mockClear() { - this.output = ''; - }; - - this.mockRestore = function mockRestore() { - console[method] = old; - }; -} - -module.exports = { - exec, -}; diff --git a/test/lib/child-script.ts b/test/lib/child-script.ts new file mode 100644 index 0000000..7e24ebe --- /dev/null +++ b/test/lib/child-script.ts @@ -0,0 +1,69 @@ +import util from 'util'; +import { OAuth2Server } from '../../src'; + +/* eslint no-console: off */ + +interface Output { + result: OAuth2Server | null + err?: unknown; + exitCode: number | undefined; + stdout: string; + stderr: string; +} + +export async function exec(scriptPath: string, args: string[]): Promise { + const argv = process.argv; /* eslint-disable-line prefer-destructuring */ + process.argv = [argv[0], scriptPath, ...args]; + + const log = ConsoleOutHook('log'); + const error = ConsoleOutHook('error'); + + const res: Output = { + result: null, + err: undefined, + exitCode: 0, + stdout: '', + stderr: '' + }; + + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + res.result = await Promise.resolve(require(scriptPath)) as OAuth2Server | null; + } catch (err) { + res.err = err; + } finally { + log.mockRestore(); + error.mockRestore(); + res.exitCode = process.exitCode; + process.exitCode = undefined; + } + + res.stdout = log.output(); + res.stderr = error.output(); + + return res; +} + +function ConsoleOutHook(method: "log" | "error") { + let entries: string[] = []; + + const old = console[method]; + console[method] = function (msg?: unknown, ...args: unknown[]): void { + entries.push(util.format(msg, ...args)); + entries.push("\n") + }; + + return { + mockClear: function mockClear() { + entries = [] + }, + + mockRestore: function mockRestore() { + console[method] = old; + }, + + output: function output() { + return entries.join('') + } + } +} diff --git a/test/oauth2-issuer.test.js b/test/oauth2-issuer.test.ts similarity index 50% rename from test/oauth2-issuer.test.js rename to test/oauth2-issuer.test.ts index 0338dd2..1fc265d 100644 --- a/test/oauth2-issuer.test.js +++ b/test/oauth2-issuer.test.ts @@ -1,20 +1,21 @@ -'use strict'; +import jwt from 'jsonwebtoken'; +import type { JWK } from 'node-jose'; -const jwt = require('jsonwebtoken'); -const OAuth2Issuer = require('../lib/oauth2-issuer'); -const testKeys = require('./keys'); +import { OAuth2Issuer } from '../src/lib/oauth2-issuer'; +import type { jwtTransform } from '../src/lib/types'; +import * as testKeys from './keys'; describe('OAuth 2 issuer', () => { - let issuer; + let issuer: OAuth2Issuer; beforeAll(async () => { issuer = new OAuth2Issuer(); issuer.url = 'https://issuer.example.com'; - await issuer.keys.add(testKeys.get('test-rsa-key.json')); - await issuer.keys.add(testKeys.get('test-rsa384-key.json')); - await issuer.keys.add(testKeys.get('test-ec-key.json')); - await issuer.keys.add(testKeys.get('test-oct-key.json')); + await issuer.keys.add(testKeys.getParsed('test-rsa-key.json')); + await issuer.keys.add(testKeys.getParsed('test-rsa384-key.json')); + await issuer.keys.add(testKeys.getParsed('test-ec-key.json')); + await issuer.keys.add(testKeys.getParsed('test-oct-key.json')); }); it('should not allow to build tokens for an unknown \'kid\'', () => { @@ -25,19 +26,21 @@ describe('OAuth 2 issuer', () => { const now = Math.floor(Date.now() / 1000); const expiresIn = 1000; - const token = issuer.buildToken(false, 'test-rsa-key', null, expiresIn); + const token = issuer.buildToken(false, 'test-rsa-key', undefined, expiresIn); expect(token).toMatch(/^[\w-]+\.[\w-]+\.$/); const decoded = jwt.decode(token, { complete: true }); + expect(typeof decoded).not.toBe("string") - expect(decoded.header).toEqual({ + const decodedObj = decoded as Record; + expect(decodedObj.header).toEqual({ alg: 'none', typ: 'JWT', kid: 'test-rsa-key', }); - const p = decoded.payload; + const p = decodedObj.payload; expect(p).toMatchObject({ iss: issuer.url, @@ -46,29 +49,32 @@ describe('OAuth 2 issuer', () => { nbf: expect.any(Number), }); - expect(p.iat).toBeGreaterThanOrEqual(now); - expect(p.exp - p.iat).toEqual(expiresIn); - expect(p.nbf).toBeLessThan(now); + const parsedP = p as { iss: string, iat: number, exp: number, nbf: number } + expect(parsedP.iat).toBeGreaterThanOrEqual(now); + expect(parsedP.exp - parsedP.iat).toEqual(expiresIn); + expect(parsedP.nbf).toBeLessThan(now); }); it.each([ ['RSA', 'test-rsa-key'], ['EC', 'test-ec-key'], ['oct', 'test-oct-key'], - ])('should be able to build %s-signed tokens', async (keyType, kid) => { + ])('should be able to build %s-signed tokens', (_keyType, kid) => { const testKey = issuer.keys.get(kid); + expect(testKey).not.toBeNull() const token = issuer.buildToken(true, kid); expect(token).toMatch(/^[\w-]+\.[\w-]+\.[\w-]+$/); - expect(() => jwt.verify(token, getSecret(testKey))).not.toThrow(); + expect(() => jwt.verify(token, getSecret(testKey!))).not.toThrow(); }); it('should be able to build signed tokens with the algorithm hinted by the key', () => { const testKey = issuer.keys.get('test-rsa384-key'); - const token = issuer.buildToken(true, testKey.kid); + expect(testKey).not.toBeNull() + const token = issuer.buildToken(true, testKey!.kid); - expect(() => jwt.verify(token, getSecret(testKey))).not.toThrow(); + expect(() => jwt.verify(token, getSecret(testKey!))).not.toThrow(); }); it.each([ @@ -78,13 +84,16 @@ describe('OAuth 2 issuer', () => { const token = issuer.buildToken(true, 'test-rsa-key', scopes); const decoded = jwt.decode(token); + expect(decoded).not.toBeNull() + expect(decoded).toHaveProperty("scope") - expect(decoded.scope).toEqual('urn:scope-1 urn:scope-2'); + const parsed = decoded as { scope: unknown } + expect(parsed.scope).toEqual('urn:scope-1 urn:scope-2'); }); it('should be able to build tokens and modify the header or the payload before signing', () => { /* eslint-disable no-param-reassign */ - const transform = (header, payload) => { + const transform: jwtTransform = (header, payload) => { header.x5t = 'a-new-value'; payload.sub = 'the-subject'; }; @@ -93,33 +102,47 @@ describe('OAuth 2 issuer', () => { const token = issuer.buildToken(true, 'test-rsa-key', transform); const decoded = jwt.decode(token, { complete: true }); + expect(decoded).not.toBeNull() - expect(decoded.header.x5t).toEqual('a-new-value'); - expect(decoded.payload.sub).toEqual('the-subject'); + expect(decoded).toMatchObject({ + header: { x5t: 'a-new-value' }, + payload: { + sub: 'the-subject' + }, + }); }); it('should be able to modify the header and the payload through a beforeSigning event', () => { issuer.once('beforeSigning', (token) => { - /* eslint-disable no-param-reassign */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access token.header.x5t = 'a-new-value'; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access token.payload.sub = 'the-subject'; - /* eslint-enable no-param-reassign */ }); const token = issuer.buildToken(true, 'test-rsa-key'); const decoded = jwt.decode(token, { complete: true }); + expect(decoded).not.toBeNull() - expect(decoded.header.x5t).toEqual('a-new-value'); - expect(decoded.payload.sub).toEqual('the-subject'); + expect(decoded).toMatchObject({ + header: { x5t: 'a-new-value' }, + payload: { + sub: 'the-subject' + }, + }); }); }); -function getSecret(key) { +function getSecret(key: JWK.Key): string { switch (key.kty) { case 'RSA': case 'EC': return key.toPEM(false); default: - return key.toObject(true).k; + { + const parsed = key.toJSON(true); + expect(parsed).toMatchObject({ k: expect.any(String) }) + return (parsed as { k: string }).k; + } } } diff --git a/test/oauth2-server.test.js b/test/oauth2-server.test.ts similarity index 84% rename from test/oauth2-server.test.js rename to test/oauth2-server.test.ts index a1e04b9..ce2ecba 100644 --- a/test/oauth2-server.test.js +++ b/test/oauth2-server.test.ts @@ -1,7 +1,5 @@ -'use strict'; - -const request = require('supertest'); -const OAuth2Server = require('../lib/oauth2-server'); +import request from 'supertest'; +import { OAuth2Server } from '../src/lib/oauth2-server'; describe('OAuth 2 Server', () => { it('should be able to start and stop the server', async () => { @@ -20,7 +18,7 @@ describe('OAuth 2 Server', () => { expect(server.issuer.url).toBeNull(); - await server.start(null, 'localhost'); + await server.start(undefined, 'localhost'); expect(server.issuer.url).toEqual(`http://localhost:${server.address().port}`); await expect(server.stop()).resolves.not.toBeDefined(); diff --git a/test/oauth2-service.test.js b/test/oauth2-service.test.ts similarity index 76% rename from test/oauth2-service.test.js rename to test/oauth2-service.test.ts index dfb5afc..e8f2ef0 100644 --- a/test/oauth2-service.test.js +++ b/test/oauth2-service.test.ts @@ -1,19 +1,19 @@ -'use strict'; +import request from 'supertest'; +import jwt from 'jsonwebtoken'; +import { IncomingMessage } from 'http'; +import type { Express } from "express"; -const request = require('supertest'); -const jwt = require('jsonwebtoken'); -const { IncomingMessage } = require('http'); -const OAuth2Issuer = require('../lib/oauth2-issuer'); -const OAuth2Service = require('../lib/oauth2-service'); -const testKeys = require('./keys'); +import { OAuth2Issuer } from '../src/lib/oauth2-issuer'; +import { OAuth2Service } from '../src/lib/oauth2-service'; +import * as testKeys from './keys'; describe('OAuth 2 service', () => { - let service; + let service: OAuth2Service; beforeAll(async () => { const issuer = new OAuth2Issuer(); issuer.url = 'https://issuer.example.com'; - await issuer.keys.add(testKeys.get('test-rsa-key.json')); + await issuer.keys.add(testKeys.getParsed('test-rsa-key.json')); service = new OAuth2Service(issuer); }); @@ -24,20 +24,21 @@ describe('OAuth 2 service', () => { .expect(200); const { url } = service.issuer; + expect(url).not.toBeNull() expect(res.body).toMatchObject({ issuer: url, - token_endpoint: `${url}/token`, - authorization_endpoint: `${url}/authorize`, - userinfo_endpoint: `${url}/userinfo`, + token_endpoint: `${url!}/token`, + authorization_endpoint: `${url!}/authorize`, + userinfo_endpoint: `${url!}/userinfo`, token_endpoint_auth_methods_supported: ['none'], - jwks_uri: `${url}/jwks`, + jwks_uri: `${url!}/jwks`, response_types_supported: ['code'], grant_types_supported: ['client_credentials', 'authorization_code', 'password'], token_endpoint_auth_signing_alg_values_supported: ['RS256'], response_modes_supported: ['query'], id_token_signing_alg_values_supported: ['RS256'], - revocation_endpoint: `${url}/revoke`, + revocation_endpoint: `${url!}/revoke`, }); }); @@ -74,11 +75,15 @@ describe('OAuth 2 service', () => { }); const key = service.issuer.keys.get('test-rsa-key'); + expect(key).not.toBeNull() - const decoded = jwt.verify(res.body.access_token, key.toPEM(false)); + const resBody = res.body as { access_token: string, scope: string } + const decoded = jwt.verify(resBody.access_token, key!.toPEM(false)); - expect(decoded.iss).toEqual(service.issuer.url); - expect(decoded.scope).toEqual(res.body.scope); + expect(decoded).toMatchObject({ + iss: service.issuer.url, + scope: resBody.scope + }) }); it('should expose a token endpoint that handles Resource Owner Password Credentials grants', async () => { @@ -100,13 +105,16 @@ describe('OAuth 2 service', () => { refresh_token: expect.any(String), }); + const resBody = res.body as { access_token: string, scope: string } + const key = service.issuer.keys.get('test-rsa-key'); + expect(key).not.toBeNull() - const decoded = jwt.verify(res.body.access_token, key.toPEM(false)); + const decoded = jwt.verify(resBody.access_token, key!.toPEM(false)); expect(decoded).toMatchObject({ iss: service.issuer.url, - scope: res.body.scope, + scope: resBody.scope, sub: 'the-resource-owner@example.com', amr: ['pwd'], }); @@ -134,8 +142,10 @@ describe('OAuth 2 service', () => { }); const key = service.issuer.keys.get('test-rsa-key'); + expect(key).not.toBeNull() - const decoded = jwt.verify(res.body.access_token, key.toPEM(false)); + const resBody = res.body as { access_token: string } + const decoded = jwt.verify(resBody.access_token, key!.toPEM(false)); expect(decoded).toMatchObject({ iss: service.issuer.url, @@ -167,8 +177,10 @@ describe('OAuth 2 service', () => { }); const key = service.issuer.keys.get('test-rsa-key'); + expect(key).not.toBeNull() - const decoded = jwt.verify(res.body.access_token, key.toPEM(false)); + const resBody = res.body as { access_token: string, scope: string, id_token: string } + const decoded = jwt.verify(resBody.access_token, key!.toPEM(false)); expect(decoded).toMatchObject({ iss: service.issuer.url, @@ -177,7 +189,7 @@ describe('OAuth 2 service', () => { amr: ['pwd'], }); - const decodedIdToken = jwt.verify(res.body.id_token, key.toPEM(false)); + const decodedIdToken = jwt.verify(resBody.id_token, key!.toPEM(false)); expect(decodedIdToken).toMatchObject({ aud: 'client_id_sample', }); @@ -204,8 +216,10 @@ describe('OAuth 2 service', () => { }); const key = service.issuer.keys.get('test-rsa-key'); + expect(key).not.toBeNull() - const decoded = jwt.verify(res.body.access_token, key.toPEM(false)); + const resBody = res.body as { access_token: string } + const decoded = jwt.verify(resBody.access_token, key!.toPEM(false)); expect(decoded).toMatchObject({ iss: service.issuer.url, @@ -232,8 +246,13 @@ describe('OAuth 2 service', () => { .expect(200); const key = service.issuer.keys.get('test-rsa-key'); + expect(key).not.toBeNull() - const decoded = jwt.verify(res.body.id_token, key.toPEM(false)); + expect(res.body).toMatchObject({ + id_token: expect.any(String) + }) + const resBody = res.body as { id_token: string } + const decoded = jwt.verify(resBody.id_token, key!.toPEM(false)); expect(decoded).toMatchObject({ sub: 'johndoe', @@ -263,8 +282,13 @@ describe('OAuth 2 service', () => { .expect(200); const key = service.issuer.keys.get('test-rsa-key'); + expect(key).not.toBeNull() - const decoded = jwt.verify(res.body.id_token, key.toPEM(false)); + expect(res.body).toMatchObject({ + id_token: expect.any(String) + }) + const resBody = res.body as { id_token: string } + const decoded = jwt.verify(resBody.id_token, key!.toPEM(false)); expect(decoded).toMatchObject({ sub: 'johndoe', @@ -300,8 +324,13 @@ describe('OAuth 2 service', () => { .expect(200); const key = service.issuer.keys.get('test-rsa-key'); + expect(key).not.toBeNull() - const decoded = jwt.verify(res.body.id_token, key.toPEM(false)); + expect(res.body).toMatchObject({ + id_token: expect.any(String) + }) + const resBody = res.body as { id_token: string } + const decoded = jwt.verify(resBody.id_token, key!.toPEM(false)); expect(decoded).toMatchObject({ sub: 'johndoe', @@ -316,7 +345,11 @@ describe('OAuth 2 service', () => { .redirects(0) .expect(302); - expect(res.headers.location).toMatch(/http:\/\/example\.com\/callback\?code=[^&]*&scope=dummy_scope&state=state123/); + expect(res).toMatchObject({ + headers: { + location: expect.stringMatching(/http:\/\/example\.com\/callback\?code=[^&]*&scope=dummy_scope&state=state123/) + } + }) }); it('should redirect to callback url with an error and keeping state when calling authorize endpoint with an invalid response type', async () => { @@ -326,7 +359,11 @@ describe('OAuth 2 service', () => { .redirects(0) .expect(302); - expect(res.headers.location).toMatch('http://example.com/callback?error=unsupported_response_type&error_description=The+authorization+server+does+not+support+obtaining+an+access+token+using+this+response_type.&state=state123'); + expect(res).toMatchObject({ + headers: { + location: 'http://example.com/callback?error=unsupported_response_type&error_description=The+authorization+server+does+not+support+obtaining+an+access+token+using+this+response_type.&state=state123' + } + }) }); it('should not handle token requests unsupported grant types', async () => { @@ -344,11 +381,12 @@ describe('OAuth 2 service', () => { it('should be able to transform the token endpoint response', async () => { service.once('beforeResponse', (tokenEndpointResponse, req) => { expect(req).toBeInstanceOf(IncomingMessage); - /* eslint-disable no-param-reassign */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access tokenEndpointResponse.body.expires_in = 9000; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access tokenEndpointResponse.body.some_stuff = 'whatever'; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access tokenEndpointResponse.statusCode = 302; - /* eslint-enable no-param-reassign */ }); const res = await request(service.requestHandler) @@ -376,9 +414,8 @@ describe('OAuth 2 service', () => { it('should allow customizing the token response through a beforeTokenSigning event', async () => { service.once('beforeTokenSigning', (token, req) => { expect(req).toBeInstanceOf(IncomingMessage); - /* eslint-disable no-param-reassign */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access token.payload.custom_header = req.headers['custom-header']; - /* eslint-enable no-param-reassign */ }); const res = await tokenRequest(service.requestHandler) @@ -390,8 +427,14 @@ describe('OAuth 2 service', () => { .expect(200); const key = service.issuer.keys.get('test-rsa-key'); + expect(key).not.toBeNull() + + expect(res.body).toMatchObject({ + access_token: expect.any(String) + }) + const resBody = res.body as { access_token: string } - const decoded = jwt.verify(res.body.access_token, key.toPEM(false)); + const decoded = jwt.verify(resBody.access_token, key!.toPEM(false)); expect(decoded).toMatchObject({ iss: service.issuer.url, @@ -413,13 +456,13 @@ describe('OAuth 2 service', () => { it('should allow customizing the userinfo response through a beforeUserinfo event', async () => { service.once('beforeUserinfo', (userInfoResponse, req) => { expect(req).toBeInstanceOf(IncomingMessage); - /* eslint-disable no-param-reassign */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access userInfoResponse.body = { error: 'invalid_token', error_message: 'token is expired', }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access userInfoResponse.statusCode = 401; - /* eslint-enable no-param-reassign */ }); const res = await request(service.requestHandler) .get('/userinfo') @@ -448,10 +491,10 @@ describe('OAuth 2 service', () => { it('should allow customizing the revoke response through a beforeRevoke event', async () => { service.once('beforeRevoke', (revokeResponse, req) => { expect(req).toBeInstanceOf(IncomingMessage); - /* eslint-disable no-param-reassign */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access revokeResponse.body = ''; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access revokeResponse.statusCode = 204; - /* eslint-enable no-param-reassign */ }); const res = await request(service.requestHandler) .post('/revoke') @@ -471,7 +514,7 @@ describe('OAuth 2 service', () => { .get('/.well-known/openid-configuration') .expect(200); - expect(res.headers['access-control-allow-origin']).toBe('*'); + expect(res).toMatchObject({ headers: { 'access-control-allow-origin': '*' } }); }); it('should expose CORS headers in an OPTIONS request', async () => { @@ -479,18 +522,20 @@ describe('OAuth 2 service', () => { .options('/token') .expect(204); - expect(res.headers['access-control-allow-origin']).toBe('*'); + expect(res).toMatchObject({ headers: { 'access-control-allow-origin': '*' } }); }); }); -function getCode(response) { - const parts = response.header.location.split('?', 2); - return parts[1].split('&') - .find((query) => query.startsWith('code=')) - .split('=', 2)[1]; +function getCode(response: request.Response) { + expect(response).toMatchObject({ + header: { location: expect.any(String) } + }) + const parsed = response as { header: { location: string } } + const url = new URL(parsed.header.location) + return url.searchParams.get('code'); } -function tokenRequest(app) { +function tokenRequest(app: Express) { return request(app) .post('/token') .type('form') diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..c368381 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "src" + ], + "exclude": [ + "test" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c77d973 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "include": [ + "src", + "test" + ], + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + "incremental": true, /* Enable incremental compilation */ + "target": "es2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + "lib": [ + "es2018" + ], /* Specify library files to be included in the compilation. */ + "allowJs": false, /* Allow javascript files to be compiled. */ + "tsBuildInfoFile": ".cache/.tsbuildinfo", + "strict": true, /* Enable all strict type-checking options. */ + "noUnusedLocals": true, /* Report errors on unused locals. */ + "noUnusedParameters": true, /* Report errors on unused parameters. */ + "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "skipLibCheck": false, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + "outDir": "./dist", + "sourceMap": false, + "declaration": true, + "removeComments": true, + } +} diff --git a/yarn.lock b/yarn.lock index 57cf034..746c8fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -463,6 +463,16 @@ source-map "^0.6.1" write-file-atomic "^3.0.0" +"@jest/types@^25.5.0": + version "25.5.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.5.0.tgz#4d6a4793f7b9599fc3680877b856a97dbccf2a9d" + integrity sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^15.0.0" + chalk "^3.0.0" + "@jest/types@^26.3.0": version "26.3.0" resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.3.0.tgz#97627bf4bdb72c55346eef98e3b3f7ddc4941f71" @@ -542,11 +552,64 @@ dependencies: "@babel/types" "^7.3.0" +"@types/basic-auth@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@types/basic-auth/-/basic-auth-1.1.3.tgz#a787ede8310804174fbbf3d6c623ab1ccedb02cd" + integrity sha512-W3rv6J0IGlxqgE2eQ2pTb0gBjaGtejQpJ6uaCjz3UQ65+TFTPC5/lAE+POfx1YLdjtxvejJzsIAfd3MxWiVmfg== + dependencies: + "@types/node" "*" + +"@types/body-parser@*", "@types/body-parser@^1.19.0": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" + integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== + dependencies: + "@types/connect" "*" + "@types/node" "*" + "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/connect@*": + version "3.4.33" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" + integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== + dependencies: + "@types/node" "*" + +"@types/cookiejar@*": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.1.tgz#90b68446364baf9efd8e8349bb36bd3852b75b80" + integrity sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw== + +"@types/cors@^2.8.7": + version "2.8.7" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.7.tgz#ab2f47f1cba93bce27dfd3639b006cc0e5600889" + integrity sha512-sOdDRU3oRS7LBNTIqwDkPJyq0lpHYcbMTt0TrjzsXbk/e37hcLTH6eZX7CdbDeN0yJJvzw9hFBZkbtCSbk/jAQ== + dependencies: + "@types/express" "*" + +"@types/express-serve-static-core@*": + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084" + integrity sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@*", "@types/express@^4.17.8": + version "4.17.8" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.8.tgz#3df4293293317e61c60137d273a2e96cd8d5f27a" + integrity sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/graceful-fs@^4.1.2": version "4.1.3" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.3.tgz#039af35fe26bec35003e8d86d2ee9c586354348f" @@ -566,6 +629,14 @@ dependencies: "@types/istanbul-lib-coverage" "*" +"@types/istanbul-reports@^1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz#e875cc689e47bce549ec81f3df5e6f6f11cfaeb2" + integrity sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw== + dependencies: + "@types/istanbul-lib-coverage" "*" + "@types/istanbul-lib-report" "*" + "@types/istanbul-reports@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz#508b13aa344fa4976234e75dddcc34925737d821" @@ -573,6 +644,14 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/jest@26.x", "@types/jest@^26.0.14": + version "26.0.14" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.14.tgz#078695f8f65cb55c5a98450d65083b2b73e5a3f3" + integrity sha512-Hz5q8Vu0D288x3iWXePSn53W7hAjP0H7EQ6QvDO9c7t46mR0lNOLlfuwQ+JkVxuhygHzlzPX+0jKdA3ZgSh+Vg== + dependencies: + jest-diff "^25.2.1" + pretty-format "^25.2.1" + "@types/json-schema@^7.0.3": version "7.0.6" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" @@ -583,11 +662,47 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/jsonwebtoken@^8.5.0": + version "8.5.0" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz#2531d5e300803aa63279b232c014acf780c981c5" + integrity sha512-9bVao7LvyorRGZCw0VmH/dr7Og+NdjYSsKAxB43OQoComFbBgsEpoR9JW6+qSq/ogwVBg8GI2MfAlk4SYI4OLg== + dependencies: + "@types/node" "*" + +"@types/lodash.isplainobject@^4.0.6": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@types/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#757d2dcdecbb32f4452018b285a586776092efd1" + integrity sha512-8G41YFhmOl8Ck6NrwLK5hhnbz6ADfuDJP+zusDnX3PoYhfC60+H/rQE6zmdO4yFzPCPJPY4oGZK2spbXm6gYEA== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.161" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.161.tgz#a21ca0777dabc6e4f44f3d07f37b765f54188b18" + integrity sha512-EP6O3Jkr7bXvZZSZYlsgt5DIjiGr0dXP1/jVEwVLTFgg0d+3lWVQkRavYVQszV7dYUwvg0B8R0MBDpcmXg7XIA== + +"@types/mime@*": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" + integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== + +"@types/node-jose@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@types/node-jose/-/node-jose-1.1.5.tgz#bad49d012c5b61a49839f6fa3b09bce33f28b1f2" + integrity sha512-fScnd7GdaHC31PYouWR0xYSOXQLrmxPhLM92CYlVy4UetSwis2u5e6khMazj1Xyidt8zPeRU0PHLmI+mpamhGQ== + dependencies: + "@types/node" "*" + "@types/node@*": version "14.11.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256" integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA== +"@types/node@^10.17.35": + version "10.17.35" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.35.tgz#58058f29b870e6ae57b20e4f6e928f02b7129f56" + integrity sha512-gXx7jAWpMddu0f7a+L+txMplp3FnHl53OhQIF9puXKq3hDGY/GjH+MF04oWnV/adPSCrbtHumDCFwzq2VhltWA== + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -598,11 +713,49 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.1.tgz#be148756d5480a84cde100324c03a86ae5739fb5" integrity sha512-2zs+O+UkDsJ1Vcp667pd3f8xearMdopz/z54i99wtRDI5KLmngk7vlrYZD0ZjKHaROR03EznlBbVY9PfAEyJIQ== +"@types/qs@*": + version "6.9.5" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" + integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== + +"@types/range-parser@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + +"@types/serve-static@*": + version "1.13.5" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.5.tgz#3d25d941a18415d3ab092def846e135a08bbcf53" + integrity sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ== + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/superagent@*": + version "4.1.10" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.10.tgz#5e2cc721edf58f64fe9b819f326ee74803adee86" + integrity sha512-xAgkb2CMWUMCyVc/3+7iQfOEBE75NvuZeezvmixbUw3nmENf2tCnQkW5yQLTYqvXUQ+R6EXxdqKKbal2zM5V/g== + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + +"@types/supertest@^2.0.10": + version "2.0.10" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.10.tgz#630d79b4d82c73e043e43ff777a9ca98d457cab7" + integrity sha512-Xt8TbEyZTnD5Xulw95GLMOkmjGICrOQyJ2jqgkSjAUR3mm7pAIzSR0NFBaMcwlzVvlpCjNwbATcWWwjNiZiFrQ== + dependencies: + "@types/superagent" "*" + +"@types/uuid@^8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f" + integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ== + "@types/yargs-parser@*": version "15.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" @@ -615,7 +768,20 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/experimental-utils@^4.0.1": +"@typescript-eslint/eslint-plugin@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.3.0.tgz#1a23d904bf8ea248d09dc3761af530d90f39c8fa" + integrity sha512-RqEcaHuEKnn3oPFislZ6TNzsBLqpZjN93G69SS+laav/I8w/iGMuMq97P0D2/2/kW4SCebHggqhbcCfbDaaX+g== + dependencies: + "@typescript-eslint/experimental-utils" "4.3.0" + "@typescript-eslint/scope-manager" "4.3.0" + debug "^4.1.1" + functional-red-black-tree "^1.0.1" + regexpp "^3.0.0" + semver "^7.3.2" + tsutils "^3.17.1" + +"@typescript-eslint/experimental-utils@4.3.0", "@typescript-eslint/experimental-utils@^4.0.1": version "4.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.3.0.tgz#3f3c6c508e01b8050d51b016e7f7da0e3aefcb87" integrity sha512-cmmIK8shn3mxmhpKfzMMywqiEheyfXLV/+yPDnOTvQX/ztngx7Lg/OD26J8gTZfkLKUmaEBxO2jYP3keV7h2OQ== @@ -627,6 +793,16 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" +"@typescript-eslint/parser@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.3.0.tgz#684fc0be6551a2bfcb253991eec3c786a8c063a3" + integrity sha512-JyfRnd72qRuUwItDZ00JNowsSlpQGeKfl9jxwO0FHK1qQ7FbYdoy5S7P+5wh1ISkT2QyAvr2pc9dAemDxzt75g== + dependencies: + "@typescript-eslint/scope-manager" "4.3.0" + "@typescript-eslint/types" "4.3.0" + "@typescript-eslint/typescript-estree" "4.3.0" + debug "^4.1.1" + "@typescript-eslint/scope-manager@4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.3.0.tgz#c743227e087545968080d2362cfb1273842cb6a7" @@ -761,6 +937,11 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -1011,6 +1192,13 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -1023,7 +1211,7 @@ buffer-equal-constant-time@1.0.1: resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= -buffer-from@^1.0.0: +buffer-from@1.x, buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== @@ -1092,6 +1280,14 @@ chalk@^2.0.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" @@ -1178,6 +1374,11 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +comment-parser@^0.7.6: + version "0.7.6" + resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-0.7.6.tgz#0e743a53c8e646c899a1323db31f6cd337b10f12" + integrity sha512-GKNxVA7/iuTnAqGADlTWX4tkhzxZKXp5fLJqKTlQLHkE65XDUKutZ3BHaJC5IGcper2tT3QRD1xr4o3jNpgXXg== + component-emitter@^1.2.1, component-emitter@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -1391,11 +1592,21 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +diff-sequences@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd" + integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg== + diff-sequences@^26.3.0: version "26.3.0" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.3.0.tgz#62a59b1b29ab7fd27cef2a33ae52abe73042d0a2" integrity sha512-5j5vdRcw3CNctePNYN0Wy2e/JbWT6cAYnXv5OuqPhDpyCGc0uLu2TK0zOCJWNB9kOIfYMSpIulRaDgIi4HJ6Ig== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -1571,6 +1782,13 @@ eslint-config-airbnb-base@^14.2.0: object.assign "^4.1.0" object.entries "^1.1.2" +eslint-config-prettier@^6.12.0: + version "6.12.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.12.0.tgz#9eb2bccff727db1c52104f0b49e87ea46605a0d2" + integrity sha512-9jWPlFlgNwRUYVoujvWTQ1aMO8o6648r+K7qU7K5Jmkbyqav1fuEZC0COYpGBxyiAJb65Ra9hrmFx19xRGwXWw== + dependencies: + get-stdin "^6.0.0" + eslint-import-resolver-node@^0.3.4: version "0.3.4" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" @@ -1613,6 +1831,26 @@ eslint-plugin-jest@^24.0.2: dependencies: "@typescript-eslint/experimental-utils" "^4.0.1" +eslint-plugin-jsdoc@^30.6.3: + version "30.6.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-30.6.3.tgz#5d946f7a27bd9ee851c67838f607d85ea0492bfa" + integrity sha512-RnyM+a3SKRfPs/jqO2qOGAEZnOJT2dOhiwhBlYVp8/yRUUBNPlvkwZm0arrnyFKvfZX6WqSwlK5OcNnM5W1Etg== + dependencies: + comment-parser "^0.7.6" + debug "^4.1.1" + jsdoctypeparser "^9.0.0" + lodash "^4.17.20" + regextras "^0.7.1" + semver "^7.3.2" + spdx-expression-parse "^3.0.1" + +eslint-plugin-prettier@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz#168ab43154e2ea57db992a2cd097c828171f75c2" + integrity sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg== + dependencies: + prettier-linter-helpers "^1.0.0" + eslint-scope@^5.0.0, eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" @@ -1877,6 +2115,11 @@ fast-deep-equal@^3.1.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-diff@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" + integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== + fast-glob@^3.1.1: version "3.2.4" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" @@ -1889,7 +2132,7 @@ fast-glob@^3.1.1: micromatch "^4.0.2" picomatch "^2.2.1" -fast-json-stable-stringify@^2.0.0: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -2069,6 +2312,11 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-stdin@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" + integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== + get-stream@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -2650,6 +2898,16 @@ jest-config@^26.4.2: micromatch "^4.0.2" pretty-format "^26.4.2" +jest-diff@^25.2.1: + version "25.5.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.5.0.tgz#1dd26ed64f96667c068cef026b677dfa01afcfa9" + integrity sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A== + dependencies: + chalk "^3.0.0" + diff-sequences "^25.2.6" + jest-get-type "^25.2.6" + pretty-format "^25.5.0" + jest-diff@^26.4.2: version "26.4.2" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.4.2.tgz#a1b7b303bcc534aabdb3bd4a7caf594ac059f5aa" @@ -2703,6 +2961,11 @@ jest-environment-node@^26.3.0: jest-mock "^26.3.0" jest-util "^26.3.0" +jest-get-type@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.2.6.tgz#0b0a32fab8908b44d508be81681487dbabb8d877" + integrity sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig== + jest-get-type@^26.3.0: version "26.3.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" @@ -2923,7 +3186,7 @@ jest-snapshot@^26.4.2: pretty-format "^26.4.2" semver "^7.3.2" -jest-util@^26.3.0: +jest-util@^26.1.0, jest-util@^26.3.0: version "26.3.0" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.3.0.tgz#a8974b191df30e2bf523ebbfdbaeb8efca535b3e" integrity sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw== @@ -2996,6 +3259,11 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= +jsdoctypeparser@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/jsdoctypeparser/-/jsdoctypeparser-9.0.0.tgz#8c97e2fb69315eb274b0f01377eaa5c940bd7b26" + integrity sha512-jrTA2jJIL6/DAEILBEh2/w9QxCuwmvNXIry39Ay/HVfhE3o2yVV0U44blYkqdHA/OKloJEqvJy0xU+GSdE2SIw== + jsdom@^16.2.2: version "16.4.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.4.0.tgz#36005bde2d136f73eee1a830c6d45e55408edddb" @@ -3058,6 +3326,13 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= +json5@2.x, json5@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" + integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== + dependencies: + minimist "^1.2.5" + json5@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" @@ -3065,13 +3340,6 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -json5@^2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" - integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== - dependencies: - minimist "^1.2.5" - jsonwebtoken@^8.5.1: version "8.5.1" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" @@ -3225,6 +3493,11 @@ lodash.isstring@^4.0.1: resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + lodash.once@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" @@ -3235,7 +3508,7 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -3252,6 +3525,11 @@ make-dir@^3.0.0: dependencies: semver "^6.0.0" +make-error@1.x, make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + makeerror@1.0.x: version "1.0.11" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" @@ -3370,6 +3648,11 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" +mkdirp@1.x, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + mkdirp@^0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" @@ -3377,11 +3660,6 @@ mkdirp@^0.5.1: dependencies: minimist "^1.2.5" -mkdirp@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -3819,6 +4097,28 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5" + integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg== + +pretty-format@^25.2.1, pretty-format@^25.5.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a" + integrity sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ== + dependencies: + "@jest/types" "^25.5.0" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^16.12.0" + pretty-format@^26.4.2: version "26.4.2" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.4.2.tgz#d081d032b398e801e2012af2df1214ef75a81237" @@ -3961,11 +4261,16 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexpp@^3.1.0: +regexpp@^3.0.0, regexpp@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== +regextras@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/regextras/-/regextras-0.7.1.tgz#be95719d5f43f9ef0b9fa07ad89b7c606995a3b2" + integrity sha512-9YXf6xtW+qzQ+hcMQXx95MOvfqXFgsKDZodX3qZB0x2n5Z94ioetIITsBtvJbiOyxa/6s9AtyweBLCdPmPko/w== + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -4145,16 +4450,16 @@ saxes@^5.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +semver@7.x, semver@^7.2.1, semver@^7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + semver@^6.0.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.2.1, semver@^7.3.2: - version "7.3.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" - integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== - send@0.17.1: version "0.17.1" resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" @@ -4298,7 +4603,7 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.5.6: +source-map-support@^0.5.17, source-map-support@^0.5.6: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== @@ -4339,7 +4644,7 @@ spdx-exceptions@^2.1.0: resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== -spdx-expression-parse@^3.0.0: +spdx-expression-parse@^3.0.0, spdx-expression-parse@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== @@ -4652,6 +4957,34 @@ tr46@^2.0.2: dependencies: punycode "^2.1.1" +ts-jest@^26.4.1: + version "26.4.1" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.4.1.tgz#08ec0d3fc2c3a39e4a46eae5610b69fafa6babd0" + integrity sha512-F4aFq01aS6mnAAa0DljNmKr/Kk9y4HVZ1m6/rtJ0ED56cuxINGq3Q9eVAh+z5vcYKe5qnTMvv90vE8vUMFxomg== + dependencies: + "@types/jest" "26.x" + bs-logger "0.x" + buffer-from "1.x" + fast-json-stable-stringify "2.x" + jest-util "^26.1.0" + json5 "2.x" + lodash.memoize "4.x" + make-error "1.x" + mkdirp "1.x" + semver "7.x" + yargs-parser "20.x" + +ts-node@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.0.0.tgz#e7699d2a110cc8c0d3b831715e417688683460b3" + integrity sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.17" + yn "3.1.1" + tsconfig-paths@^3.9.0: version "3.9.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" @@ -4735,6 +5068,11 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typescript@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.3.tgz#153bbd468ef07725c1df9c77e8b453f8d36abba5" + integrity sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg== + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" @@ -4963,6 +5301,11 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== +yargs-parser@20.x: + version "20.2.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.1.tgz#28f3773c546cdd8a69ddae68116b48a5da328e77" + integrity sha512-yYsjuSkjbLMBp16eaOt7/siKTjNVjMm3SoJnIg3sEh/JsvqVVDyjRKmaJV4cl+lNIgq6QEco2i3gDebJl7/vLA== + yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" @@ -4987,3 +5330,8 @@ yargs@^15.3.1: which-module "^2.0.0" y18n "^4.0.0" yargs-parser "^18.1.2" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==