@sap/xssec
Advanced tools
Comparing version 3.3.5 to 3.4.0
# Change Log | ||
All notable changes to this project will be documented in this file. | ||
## 3.4.0 - 2023-10-23 | ||
- add optional x5t validation (RFC 8705) for IAS tokens | ||
- Restore support for disableCache flag for JWKS retrieval | ||
- Bugfix for requests to XSUAA with array values inside form | ||
## 3.3.5 - 2023-09-28 | ||
@@ -5,0 +10,0 @@ - Support for app2service and app2app for IAS |
@@ -86,2 +86,30 @@ // Scope Prefix: | ||
configurable: false | ||
}); | ||
Object.defineProperty(exports, "FWD_CLIENT_CERT_HEADER", { | ||
value: "x-forwarded-client-cert", | ||
enumerable: true, | ||
writable: false, | ||
configurable: false | ||
}); | ||
Object.defineProperty(exports, "CNF_X5T_CLAIM", { | ||
value: "x5t#S256", | ||
enumerable: true, | ||
writable: false, | ||
configurable: false | ||
}); | ||
Object.defineProperty(exports, "PEM_HEADER", { | ||
value: "-----BEGIN CERTIFICATE-----", | ||
enumerable: true, | ||
writable: false, | ||
configurable: false | ||
}); | ||
Object.defineProperty(exports, "PEM_FOOTER", { | ||
value: "-----END CERTIFICATE-----", | ||
enumerable: true, | ||
writable: false, | ||
configurable: false | ||
}); |
@@ -56,3 +56,3 @@ 'use strict'; | ||
this.verifyToken = function (encodedToken, attributes, cb) { | ||
const validator = new JwtTokenValidatorIAS(configArr, config); | ||
const validator = new JwtTokenValidatorIAS(configArr, config, attributes); | ||
@@ -59,0 +59,0 @@ validator.validateToken(encodedToken, function (err, tokenInfo) { |
@@ -343,3 +343,3 @@ 'use strict'; | ||
this.verifyToken = function (encodedToken, attributes, cb) { | ||
const validator = new JwtTokenValidatorUAA(configArr, config); | ||
const validator = new JwtTokenValidatorUAA(configArr, config, attributes); | ||
@@ -346,0 +346,0 @@ validator.validateToken(encodedToken, function (err, tokenInfo) { |
@@ -344,3 +344,3 @@ 'use strict'; | ||
this.verifyToken = function (encodedToken, attributes, cb) { | ||
const validator = new JwtTokenValidatorXSUAA(configArr, config); | ||
const validator = new JwtTokenValidatorXSUAA(configArr, config, attributes); | ||
@@ -347,0 +347,0 @@ validator.validateToken(encodedToken, function (err, tokenInfo) { |
@@ -36,7 +36,7 @@ 'use strict'; | ||
} | ||
return this; | ||
} | ||
getXsuaaJwks(jku, zid) { | ||
getXsuaaJwks(jku, zid, attributes = {}) { | ||
if (!jku) { | ||
@@ -46,16 +46,23 @@ throw new Error("Cannot get JWKS from empty JKU."); | ||
const keyParts = [jku, zid || ""]; | ||
const replicaKey = this.createCacheKey(keyParts); | ||
const jwksParams = { zid }; | ||
if (!this.#replicas.has(replicaKey)) { | ||
let jwksReplica, replicaKey; | ||
if (!attributes.disableCache) { | ||
const keyParts = {jku, ...jwksParams}; | ||
replicaKey = this.createCacheKey(keyParts); | ||
jwksReplica = this.#replicas.get(replicaKey); | ||
} | ||
if (!jwksReplica) { | ||
const xsuaaService = new XsuaaService(jku, zid); | ||
const jwksReplica = new JwksReplica(xsuaaService, this.expirationTime, this.refreshPeriod); | ||
jwksReplica = new JwksReplica(xsuaaService, this.expirationTime, this.refreshPeriod).withParams(jwksParams); | ||
this.#replicas.set(replicaKey, jwksReplica); | ||
!attributes.disableCache && this.#replicas.set(replicaKey, jwksReplica); | ||
} | ||
return this.#replicas.get(replicaKey); | ||
return jwksReplica; | ||
} | ||
getIdentityJwks(url, serviceCredentials, token) { | ||
getIdentityJwks(url, serviceCredentials, token, attributes = {}) { | ||
if (!url) { | ||
@@ -66,31 +73,42 @@ throw new Error("Cannot get JWKS from empty URL."); | ||
const jwksParams = { | ||
app_tid : token.getAppTID(), | ||
client_id: serviceCredentials.clientid, | ||
app_tid: token.getAppTID(), | ||
azp: token.getAzp() | ||
} | ||
const keyParts = [url, serviceCredentials.clientid, jwksParams.app_tid, jwksParams.azp]; | ||
const replicaKey = this.createCacheKey(keyParts); | ||
let jwksReplica, replicaKey; | ||
if (!attributes.disableCache) { | ||
const keyParts = {url, ...jwksParams}; | ||
replicaKey = this.createCacheKey(keyParts); | ||
if (!this.#replicas.has(replicaKey)) { | ||
jwksReplica = this.#replicas.get(replicaKey); | ||
} | ||
if (!jwksReplica) { | ||
const service = new IdentityService(serviceCredentials).withCustomDomain(url); | ||
const jwksReplica = new JwksReplica(service, this.expirationTime, this.refreshPeriod).withParams(jwksParams); | ||
jwksReplica = new JwksReplica(service, this.expirationTime, this.refreshPeriod).withParams(jwksParams); | ||
this.#replicas.set(replicaKey, jwksReplica); | ||
!attributes.disableCache && this.#replicas.set(replicaKey, jwksReplica); | ||
} | ||
return this.#replicas.get(replicaKey); | ||
return jwksReplica; | ||
} | ||
createCacheKey(parts) { | ||
if (!parts || parts.length < 1) { | ||
/** | ||
* Creates a string cache key from the given key-value pairs, ignoring keys with null or undefined values. | ||
* @param {object} parts | ||
* @returns a cache key in string format, e.g. app_tid:foo:client_id:bar:azp:baz | ||
*/ | ||
createCacheKey(parts) { | ||
if (!parts || Object.entries(parts).length < 1) { | ||
throw new Error("Could not create JwksManager key. Key parts must contain at least one element."); | ||
} | ||
return parts.map(s => { | ||
s ??= ""; | ||
return `${s.length}:${s}`; | ||
}).join(":"); | ||
return Object.entries(parts) | ||
.filter(([value]) => value != null) | ||
.map(([key, value]) => `${key}:${value}`) | ||
.join(":"); | ||
} | ||
} | ||
module.exports = JwksManager; | ||
module.exports = JwksManager; |
@@ -116,3 +116,3 @@ 'use strict'; | ||
this.#jwksUpdate = undefined; | ||
}; | ||
} | ||
} | ||
@@ -119,0 +119,0 @@ } |
@@ -18,2 +18,4 @@ 'use strict'; | ||
const IAS = "ias"; | ||
const XSUAA = "xsuaa"; | ||
const X_ZONE_ID_HEADER_NAME = "x-zid"; | ||
@@ -26,19 +28,23 @@ const CORRELATIONID_HEADER = "x-vcap-request-id"; | ||
function toFormArray(obj) { | ||
let ret = []; | ||
for (let n in obj) { | ||
const val = obj[n]; | ||
if (val !== null && val !== undefined) { | ||
if (Array.isArray(val)) { | ||
for (let i = 0; i < val.length; ++i) { | ||
ret.push([n, val[i]]); | ||
} | ||
} else if (typeof val === 'object') { | ||
ret.push([n, JSON.stringify(val)]); | ||
} else { | ||
ret.push([n, val]); | ||
} | ||
/** | ||
* Converts form parameters object to an array of key value pairs. | ||
* Parameters with array values are converted to multiple key-value pairs with the same key. | ||
* Parameters with object values are converted to a key-value pair with the object value stringified to JSON. | ||
* @param {*} params key-value object with type of values being string|Array|object | ||
* @returns array of key-value pairs | ||
*/ | ||
function toFormArray(params) { | ||
return Object.entries(params) | ||
.filter(([value]) => value != null) | ||
.flatMap(([key, value]) => { | ||
if(Array.isArray(value)) { | ||
return value.map(arrayValue => [key, arrayValue]); | ||
} | ||
} | ||
return ret; | ||
if(typeof value === 'object') { | ||
return [[key, JSON.stringify(value)]]; | ||
} | ||
return [[key, value]]; | ||
}); | ||
} | ||
@@ -77,3 +83,4 @@ | ||
if (options.form) { | ||
axios_options.data = new url.URLSearchParams(toFormArray(options.form)).toString(); | ||
const formData = options.configType?.toLowerCase() === IAS ? toFormArray(options.form) : options.form; | ||
axios_options.data = new url.URLSearchParams(formData).toString(); | ||
} | ||
@@ -159,3 +166,3 @@ | ||
if (zoneId && type !== "ias") { | ||
if (zoneId && type !== IAS) { | ||
ret[X_ZONE_ID_HEADER_NAME] = zoneId; | ||
@@ -181,3 +188,4 @@ } | ||
}, | ||
timeout: attributes.timeout | ||
timeout: attributes.timeout, | ||
configType: attributes.configType | ||
}; | ||
@@ -280,7 +288,7 @@ | ||
if (config.type === "IAS") { | ||
config.type = "ias"; | ||
config.type = IAS; | ||
} | ||
return { | ||
configType: config.type || "xsuaa", | ||
configType: config.type || XSUAA, | ||
scopes: config.scopes, | ||
@@ -297,3 +305,3 @@ correlationId: config.correlationId, | ||
timeout: defaultTimeout, | ||
configType: "xsuaa" | ||
configType: XSUAA | ||
}; | ||
@@ -300,0 +308,0 @@ } |
@@ -54,3 +54,6 @@ 'use strict'; | ||
async fetchJwks(params = {}) { | ||
params.client_id = this.serviceCredentials.clientid; | ||
if(params.client_id !== this.serviceCredentials.clientid) { | ||
return Promise.reject("Invalid state: IdentityService#fetchJwks called with client_id value that is different from the client_id of the IdentityService object."); | ||
} | ||
await this.fetchOidcInfo(); | ||
@@ -100,3 +103,3 @@ const jwksEndpoint = this.#oidcInfo["jwks_uri"]; | ||
return { | ||
domain: this.domain | ||
domain: this.url | ||
} | ||
@@ -103,0 +106,0 @@ } |
@@ -26,6 +26,7 @@ 'use strict'; | ||
var debugError = debug('xssec:jwtstrategy'); | ||
debugError.log = console.error.bind(console); | ||
debugTrace.log = console.log.bind(console); | ||
const { FWD_CLIENT_CERT_HEADER } = require('../constants'); | ||
exports.JWTStrategy = JWTStrategy; | ||
@@ -41,3 +42,3 @@ | ||
const errobj = new Error(errorStr); | ||
this.getErrorObject = function() { | ||
this.getErrorObject = function () { | ||
return errobj; | ||
@@ -74,2 +75,3 @@ } | ||
const correlationId = req.headers["x-correlationid"] || req.headers["x-vcap-request-id"]; | ||
const x509Certificate = req.headers[FWD_CLIENT_CERT_HEADER]; | ||
@@ -81,3 +83,5 @@ try { | ||
if (err) { | ||
if (err) { | ||
req.tokenInfo ??= new SimpleError(err.toString()); | ||
return err.statuscode ? self.fail(err.statuscode, err) : self.error(err); | ||
@@ -115,4 +119,8 @@ } | ||
var paramB = this._forceType ? callback : undefined; | ||
xssec.createSecurityContext(token, {credentials: this.options, correlationId: correlationId}, paramA, paramB); | ||
const config = { ...options, credentials: this.options, correlationId }; | ||
if(x509Certificate) { | ||
config.x509Certificate = x509Certificate; | ||
} | ||
xssec.createSecurityContext(token, config, paramA, paramB); | ||
} | ||
@@ -119,0 +127,0 @@ catch (err) { |
@@ -10,3 +10,2 @@ 'use strict'; | ||
const jwt = require('jsonwebtoken'); | ||
const url = require('url'); | ||
@@ -16,34 +15,11 @@ | ||
const TokenExchanger = require('./tokenexchanger'); | ||
const ValidationResults = require("./validator/ValidationResults"); | ||
const X5tValidator = require("./validator/X5tValidator"); | ||
const JwksManager = require('./jwks/JwksManager'); | ||
const JwksReplica = require('./jwks/JwksReplica'); | ||
let jwksManager; | ||
const DOT = "."; | ||
const ValidationResults = new function () { | ||
function Result(suc, desc) { | ||
var state = suc; | ||
var description = desc || ""; | ||
this.isValid = function () { | ||
return suc === true; | ||
} | ||
this.isErroneous = function () { | ||
return suc === false; | ||
} | ||
this.getErrorDescription = function () { | ||
return description; | ||
} | ||
}; | ||
this.createValid = function () { | ||
return new Result(true); | ||
} | ||
this.createInvalid = function (description) { | ||
return new Result(false, description) | ||
} | ||
}; | ||
function JwtAudienceValidator(clientId) { | ||
@@ -166,3 +142,3 @@ var clientIds = []; | ||
} | ||
}; | ||
} | ||
@@ -180,3 +156,3 @@ function buildJwksManager(config) { | ||
function JwtTokenValidatorIAS(configArray, serviceCredentials) { | ||
function JwtTokenValidatorIAS(configArray, serviceCredentials, attributes) { | ||
if(!jwksManager) { | ||
@@ -191,3 +167,3 @@ jwksManager = buildJwksManager(serviceCredentials.jwksCache); | ||
function checkIssuer(issuer, domains) { | ||
if(!issuer) { | ||
if (!issuer) { | ||
throw "issuer is empty"; | ||
@@ -197,28 +173,28 @@ } | ||
//make sure we have a protocol at the beginning of the url | ||
if(issuer.indexOf('http') !== 0) { | ||
if (issuer.indexOf('http') !== 0) { | ||
issuer = "https://" + issuer; | ||
} | ||
const myURL = new url.URL(issuer); | ||
if(myURL.protocol !== 'https:') { | ||
if(myURL.hostname !== 'localhost') { | ||
if (myURL.protocol !== 'https:') { | ||
if (myURL.hostname !== 'localhost') { | ||
throw "Issuer has wrong protocol (" + myURL.protocol + ")"; | ||
} | ||
} | ||
} | ||
if(myURL.hash) { | ||
if (myURL.hash) { | ||
throw "Issuer has unallowed hash value (" + myURL.hash + ")"; | ||
} | ||
if(myURL.search) { | ||
if (myURL.search) { | ||
throw "Issuer has unallowed query value (" + myURL.search + ")"; | ||
} | ||
for(let i=0;i<domains.length;++i) { | ||
if(myURL.hostname.endsWith(domains[i])) { | ||
for (let i = 0; i < domains.length; ++i) { | ||
if (myURL.hostname.endsWith(domains[i])) { | ||
return; | ||
} | ||
} | ||
throw "Issuer not found in domain list " + domains; | ||
@@ -243,10 +219,22 @@ } | ||
checkIssuer(issuer, domains) | ||
} catch(e) { | ||
} catch (e) { | ||
return returnError(401, "Issuer validation failed (iss=" + issuer + ") message=" + e); | ||
} | ||
// (optional) x5t validation | ||
if (attributes["x5tValidation"]) { | ||
if (!attributes.x509Certificate) { | ||
return returnError(401, "x5t validation failed (no client certificate found in request header)."); | ||
} else { | ||
let x5tValidationResult = X5tValidator.validateToken(token, attributes.x509Certificate); | ||
if (!x5tValidationResult.isValid()) { | ||
return returnError(401, x5tValidationResult.getErrorDescription()); | ||
} | ||
} | ||
} | ||
const verificationKeySupplier = async (header, callback) => { | ||
let err, jwk; | ||
try { | ||
const jwks = await jwksManager.getIdentityJwks(issuer, serviceCredentials, token); | ||
const jwks = await jwksManager.getIdentityJwks(issuer, serviceCredentials, token, attributes); | ||
jwk = await jwks.get(header.kid); | ||
@@ -265,3 +253,3 @@ } catch(e) { | ||
return cb(err); | ||
} | ||
} | ||
@@ -280,3 +268,3 @@ //Audience validation | ||
} | ||
cb(null, token); | ||
@@ -289,3 +277,3 @@ }); | ||
if (!uaaDomain) { | ||
throw new Error("JKU could not be validated because attribute \'uaadomain\' is missing from service credentials."); | ||
throw new Error("JKU could not be validated because attribute 'uaadomain' is missing from service credentials."); | ||
} | ||
@@ -321,3 +309,3 @@ | ||
function JwtTokenValidatorXSUAA(configArray, serviceCredentials) { | ||
function JwtTokenValidatorXSUAA(configArray, serviceCredentials, attributes) { | ||
if(!jwksManager) { | ||
@@ -369,3 +357,3 @@ jwksManager = buildJwksManager(serviceCredentials.jwksCache); | ||
try { | ||
const jwks = jwksManager.getXsuaaJwks(header.jku, token.getZoneId()); | ||
const jwks = jwksManager.getXsuaaJwks(header.jku, token.getZoneId(), attributes); | ||
jwk = await jwks.get(header.kid); | ||
@@ -440,5 +428,5 @@ } catch(e) { | ||
}; | ||
}; | ||
} | ||
function JwtTokenValidatorUAA(configArray, serviceCredentials) { | ||
function JwtTokenValidatorUAA(configArray, serviceCredentials, attributes) { | ||
if(!jwksManager) { | ||
@@ -490,3 +478,3 @@ jwksManager = buildJwksManager(serviceCredentials.jwksCache); | ||
try { | ||
const jwks = jwksManager.getXsuaaJwks(header.jku, token.getZoneId()); | ||
const jwks = jwksManager.getXsuaaJwks(header.jku, token.getZoneId(), attributes); | ||
jwk = await jwks.get(header.kid); | ||
@@ -561,3 +549,3 @@ } catch(e) { | ||
}; | ||
}; | ||
} | ||
@@ -564,0 +552,0 @@ module.exports = { |
{ | ||
"name": "@sap/xssec", | ||
"version": "3.3.5", | ||
"version": "3.4.0", | ||
"description": "XS Advanced Container Security API for node.js", | ||
@@ -43,4 +43,5 @@ "main": "./lib", | ||
"node-rsa": "^1.1.1", | ||
"rewire": "^7.0.0", | ||
"valid-url": "1.0.9" | ||
} | ||
} |
@@ -201,2 +201,17 @@ @sap/xssec: XS Advanced Container Security API for node.js | ||
### x5t Token Validation | ||
The library optionaly supports token ownership validation via x5t thumbprint ([RFC 8705](https://datatracker.ietf.org/doc/html/rfc8705)) for tokens issues by SAP Identity service. | ||
:grey_exclamation: x5t token validation should only be enabled for applications using mTLS because the x5t validation will fail when there is no client certificate used for the request. SAP BTP will automatically put the client certificate in the `x-forwarded-client-cert` header of requests performed against `cert` application routes. From there it will be picked up by this lib to do the validation against the fingerprint claim from the token payload. | ||
To enable x5t validation, pass a truthy value for the `x5tValidation` flag in the configuration object: | ||
```js | ||
// when creating securityContext manually | ||
xssec.createSecurityContext(access_token, { x5tValidation: true }, function(error, securityContext, tokenInfo) { ... }); | ||
// when using passport | ||
app.use(passport.authenticate('JWT', { x5tValidation: true })); | ||
``` | ||
### Test Usage without having an Access Token | ||
@@ -203,0 +218,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
158383
30
2467
475
7
+ Addedrewire@^7.0.0
+ Added@eslint-community/eslint-utils@4.4.1(transitive)
+ Added@eslint-community/regexpp@4.12.1(transitive)
+ Added@eslint/eslintrc@2.1.4(transitive)
+ Added@eslint/js@8.57.1(transitive)
+ Added@humanwhocodes/config-array@0.13.0(transitive)
+ Added@humanwhocodes/module-importer@1.0.1(transitive)
+ Added@humanwhocodes/object-schema@2.0.3(transitive)
+ Added@nodelib/fs.scandir@2.1.5(transitive)
+ Added@nodelib/fs.stat@2.0.5(transitive)
+ Added@nodelib/fs.walk@1.2.8(transitive)
+ Added@ungap/structured-clone@1.2.0(transitive)
+ Addedacorn@8.14.0(transitive)
+ Addedacorn-jsx@5.3.2(transitive)
+ Addedajv@6.12.6(transitive)
+ Addedansi-regex@5.0.1(transitive)
+ Addedansi-styles@4.3.0(transitive)
+ Addedargparse@2.0.1(transitive)
+ Addedbalanced-match@1.0.2(transitive)
+ Addedbrace-expansion@1.1.11(transitive)
+ Addedcallsites@3.1.0(transitive)
+ Addedchalk@4.1.2(transitive)
+ Addedcolor-convert@2.0.1(transitive)
+ Addedcolor-name@1.1.4(transitive)
+ Addedconcat-map@0.0.1(transitive)
+ Addedcross-spawn@7.0.5(transitive)
+ Addeddeep-is@0.1.4(transitive)
+ Addeddoctrine@3.0.0(transitive)
+ Addedescape-string-regexp@4.0.0(transitive)
+ Addedeslint@8.57.1(transitive)
+ Addedeslint-scope@7.2.2(transitive)
+ Addedeslint-visitor-keys@3.4.3(transitive)
+ Addedespree@9.6.1(transitive)
+ Addedesquery@1.6.0(transitive)
+ Addedesrecurse@4.3.0(transitive)
+ Addedestraverse@5.3.0(transitive)
+ Addedesutils@2.0.3(transitive)
+ Addedfast-deep-equal@3.1.3(transitive)
+ Addedfast-json-stable-stringify@2.1.0(transitive)
+ Addedfast-levenshtein@2.0.6(transitive)
+ Addedfastq@1.17.1(transitive)
+ Addedfile-entry-cache@6.0.1(transitive)
+ Addedfind-up@5.0.0(transitive)
+ Addedflat-cache@3.2.0(transitive)
+ Addedflatted@3.3.1(transitive)
+ Addedfs.realpath@1.0.0(transitive)
+ Addedglob@7.2.3(transitive)
+ Addedglob-parent@6.0.2(transitive)
+ Addedglobals@13.24.0(transitive)
+ Addedgraphemer@1.4.0(transitive)
+ Addedhas-flag@4.0.0(transitive)
+ Addedignore@5.3.2(transitive)
+ Addedimport-fresh@3.3.0(transitive)
+ Addedimurmurhash@0.1.4(transitive)
+ Addedinflight@1.0.6(transitive)
+ Addedinherits@2.0.4(transitive)
+ Addedis-extglob@2.1.1(transitive)
+ Addedis-glob@4.0.3(transitive)
+ Addedis-path-inside@3.0.3(transitive)
+ Addedisexe@2.0.0(transitive)
+ Addedjs-yaml@4.1.0(transitive)
+ Addedjson-buffer@3.0.1(transitive)
+ Addedjson-schema-traverse@0.4.1(transitive)
+ Addedjson-stable-stringify-without-jsonify@1.0.1(transitive)
+ Addedkeyv@4.5.4(transitive)
+ Addedlevn@0.4.1(transitive)
+ Addedlocate-path@6.0.0(transitive)
+ Addedlodash.merge@4.6.2(transitive)
+ Addedminimatch@3.1.2(transitive)
+ Addednatural-compare@1.4.0(transitive)
+ Addedonce@1.4.0(transitive)
+ Addedoptionator@0.9.4(transitive)
+ Addedp-limit@3.1.0(transitive)
+ Addedp-locate@5.0.0(transitive)
+ Addedparent-module@1.0.1(transitive)
+ Addedpath-exists@4.0.0(transitive)
+ Addedpath-is-absolute@1.0.1(transitive)
+ Addedpath-key@3.1.1(transitive)
+ Addedprelude-ls@1.2.1(transitive)
+ Addedpunycode@2.3.1(transitive)
+ Addedqueue-microtask@1.2.3(transitive)
+ Addedresolve-from@4.0.0(transitive)
+ Addedreusify@1.0.4(transitive)
+ Addedrewire@7.0.0(transitive)
+ Addedrimraf@3.0.2(transitive)
+ Addedrun-parallel@1.2.0(transitive)
+ Addedshebang-command@2.0.0(transitive)
+ Addedshebang-regex@3.0.0(transitive)
+ Addedstrip-ansi@6.0.1(transitive)
+ Addedstrip-json-comments@3.1.1(transitive)
+ Addedsupports-color@7.2.0(transitive)
+ Addedtext-table@0.2.0(transitive)
+ Addedtype-check@0.4.0(transitive)
+ Addedtype-fest@0.20.2(transitive)
+ Addeduri-js@4.4.1(transitive)
+ Addedwhich@2.0.2(transitive)
+ Addedword-wrap@1.2.5(transitive)
+ Addedwrappy@1.0.2(transitive)
+ Addedyocto-queue@0.1.0(transitive)