@frontegg/client
Advanced tools
Comparing version 4.2.0-alpha.2 to 4.2.0-alpha.3
@@ -1,2 +0,2 @@ | ||
import { IUser } from '../../../middlewares'; | ||
import { IUser } from "../../identity/types"; | ||
export interface CodeExchangeResponse { | ||
@@ -3,0 +3,0 @@ /** |
@@ -1,3 +0,3 @@ | ||
import { IFronteggContext } from '../../components/frontegg-context'; | ||
import { IUser, IWithAuthenticationOptions } from '../../middlewares'; | ||
import { AuthHeaderType, IValidateTokenOptions, TEntity } from './types'; | ||
import { IFronteggContext } from '../../components/frontegg-context/types'; | ||
export declare class IdentityClient { | ||
@@ -10,4 +10,4 @@ static getInstance(): IdentityClient; | ||
getPublicKey(): Promise<string>; | ||
validateIdentityOnToken(token: string, options?: IWithAuthenticationOptions): Promise<IUser>; | ||
validateIdentityOnToken(token: string, options?: IValidateTokenOptions, type?: AuthHeaderType): Promise<TEntity>; | ||
private fetchPublicKey; | ||
} |
@@ -41,3 +41,2 @@ "use strict"; | ||
var axios_1 = require("axios"); | ||
var jsonwebtoken_1 = require("jsonwebtoken"); | ||
var authenticator_1 = require("../../authenticator"); | ||
@@ -47,2 +46,6 @@ var config_1 = require("../../config"); | ||
var frontegg_context_1 = require("../../components/frontegg-context"); | ||
var types_1 = require("./types"); | ||
var token_resolvers_1 = require("./token-resolvers"); | ||
var failed_to_authenticate_exception_1 = require("./exceptions/failed-to-authenticate.exception"); | ||
var tokenResolvers = [token_resolvers_1.authorizationHeaderResolver, token_resolvers_1.accessTokenHeaderResolver]; | ||
var IdentityClient = /** @class */ (function () { | ||
@@ -76,80 +79,40 @@ function IdentityClient(context) { | ||
}; | ||
IdentityClient.prototype.validateIdentityOnToken = function (token, options) { | ||
IdentityClient.prototype.validateIdentityOnToken = function (token, options, type) { | ||
if (type === void 0) { type = types_1.AuthHeaderType.JWT; } | ||
return __awaiter(this, void 0, void 0, function () { | ||
var _this = this; | ||
var publicKey, e_1, resolver, entity; | ||
return __generator(this, function (_a) { | ||
/* eslint-disable no-async-promise-executor */ | ||
return [2 /*return*/, new Promise(function (resolve, reject) { return __awaiter(_this, void 0, void 0, function () { | ||
var publicKey, e_1; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
try { | ||
token = token.replace('Bearer ', ''); | ||
} | ||
catch (e) { | ||
logger_1.default.error('Failed to extract token - ', token); | ||
reject({ statusCode: 401, message: 'Failed to verify authentication' }); | ||
return [2 /*return*/]; | ||
} | ||
_a.label = 1; | ||
case 1: | ||
_a.trys.push([1, 3, , 4]); | ||
return [4 /*yield*/, this.getPublicKey()]; | ||
case 2: | ||
publicKey = _a.sent(); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
e_1 = _a.sent(); | ||
logger_1.default.error('failed to get public key - ', e_1); | ||
reject({ statusCode: 401, message: 'Failed to verify authentication' }); | ||
return [2 /*return*/]; | ||
case 4: | ||
(0, jsonwebtoken_1.verify)(token, publicKey, { algorithms: ['RS256'] }, function (err, decoded) { | ||
var user = decoded; | ||
if (err) { | ||
logger_1.default.error('Failed to verify jwt - ', err); | ||
reject({ statusCode: 401, message: 'Failed to verify authentication' }); | ||
return; | ||
} | ||
if (options) { | ||
var roles = options.roles, permissions = options.permissions; | ||
if (roles && roles.length > 0) { | ||
var haveAtLeastOneRole = false; | ||
for (var _i = 0, roles_1 = roles; _i < roles_1.length; _i++) { | ||
var requestedRole = roles_1[_i]; | ||
if (user.roles && user.roles.includes(requestedRole)) { | ||
haveAtLeastOneRole = true; | ||
break; | ||
} | ||
} | ||
if (!haveAtLeastOneRole) { | ||
logger_1.default.info('Insufficient role'); | ||
reject({ statusCode: 403, message: 'Insufficient role' }); | ||
return; | ||
} | ||
} | ||
if (permissions && permissions.length > 0) { | ||
var haveAtLeastOnePermission = false; | ||
for (var _a = 0, permissions_1 = permissions; _a < permissions_1.length; _a++) { | ||
var requestedPermission = permissions_1[_a]; | ||
if (user.permissions && user.permissions.includes(requestedPermission)) { | ||
haveAtLeastOnePermission = true; | ||
break; | ||
} | ||
} | ||
if (!haveAtLeastOnePermission) { | ||
logger_1.default.info('Insufficient permission'); | ||
reject({ statusCode: 403, message: 'Insufficient permission' }); | ||
return; | ||
} | ||
} | ||
} | ||
// Store the decoded user on the request | ||
resolve(user); | ||
}); | ||
return [2 /*return*/]; | ||
switch (_a.label) { | ||
case 0: | ||
if (type === types_1.AuthHeaderType.JWT) { | ||
try { | ||
token = token.replace('Bearer ', ''); | ||
} | ||
}); | ||
}); })]; | ||
catch (e) { | ||
logger_1.default.error('Failed to extract token - ', token); | ||
throw new failed_to_authenticate_exception_1.FailedToAuthenticateException(); | ||
} | ||
} | ||
_a.label = 1; | ||
case 1: | ||
_a.trys.push([1, 3, , 4]); | ||
return [4 /*yield*/, this.getPublicKey()]; | ||
case 2: | ||
publicKey = _a.sent(); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
e_1 = _a.sent(); | ||
logger_1.default.error('failed to get public key - ', e_1); | ||
throw new failed_to_authenticate_exception_1.FailedToAuthenticateException(); | ||
case 4: | ||
resolver = tokenResolvers.find(function (resolver) { return resolver.shouldHandle(type); }); | ||
if (!resolver) { | ||
logger_1.default.error('Failed to find token resolver'); | ||
throw new failed_to_authenticate_exception_1.FailedToAuthenticateException(); | ||
} | ||
return [4 /*yield*/, resolver.validateToken(token, publicKey, options)]; | ||
case 5: | ||
entity = _a.sent(); | ||
return [2 /*return*/, entity]; | ||
} | ||
}); | ||
@@ -156,0 +119,0 @@ }); |
@@ -1,12 +0,15 @@ | ||
export interface IFronteggContext { | ||
FRONTEGG_CLIENT_ID: string; | ||
FRONTEGG_API_KEY: string; | ||
} | ||
import { IFronteggContext, IFronteggOptions } from './types'; | ||
export declare class FronteggContext { | ||
static getInstance(): FronteggContext; | ||
static init(context: IFronteggContext): void; | ||
static init(context: IFronteggContext, options?: IFronteggOptions): void; | ||
static getContext(): IFronteggContext; | ||
static getOptions(): IFronteggOptions; | ||
private static instance; | ||
private context; | ||
private options; | ||
private constructor(); | ||
private validateOptions; | ||
private validateAccessTokensOptions; | ||
private validateIORedisOptions; | ||
private validateRedisOptions; | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.FronteggContext = void 0; | ||
var package_loader_1 = require("../../utils/package-loader"); | ||
var FronteggContext = /** @class */ (function () { | ||
function FronteggContext() { | ||
this.context = null; | ||
this.options = {}; | ||
} | ||
@@ -14,4 +16,6 @@ FronteggContext.getInstance = function () { | ||
}; | ||
FronteggContext.init = function (context) { | ||
FronteggContext.init = function (context, options) { | ||
FronteggContext.getInstance().context = context; | ||
FronteggContext.getInstance().validateOptions(options); | ||
FronteggContext.getInstance().options = options !== null && options !== void 0 ? options : {}; | ||
}; | ||
@@ -24,2 +28,39 @@ FronteggContext.getContext = function () { | ||
}; | ||
FronteggContext.getOptions = function () { | ||
return FronteggContext.getInstance().options || {}; | ||
}; | ||
FronteggContext.prototype.validateOptions = function (options) { | ||
if (options === null || options === void 0 ? void 0 : options.accessTokensOptions) { | ||
this.validateAccessTokensOptions(options.accessTokensOptions); | ||
} | ||
}; | ||
FronteggContext.prototype.validateAccessTokensOptions = function (accessTokensOptions) { | ||
if (!accessTokensOptions.cache) { | ||
throw new Error("'cache' is missing from access tokens options"); | ||
} | ||
if (accessTokensOptions.cache.type === 'ioredis') { | ||
this.validateIORedisOptions(accessTokensOptions.cache.options); | ||
} | ||
else if (accessTokensOptions.cache.type === 'redis') { | ||
this.validateRedisOptions(accessTokensOptions.cache.options); | ||
} | ||
}; | ||
FronteggContext.prototype.validateIORedisOptions = function (redisOptions) { | ||
package_loader_1.PackageUtils.loadPackage('ioredis'); | ||
var requiredProperties = ['host', 'password', 'port', 'db']; | ||
requiredProperties.forEach(function (requiredProperty) { | ||
if (redisOptions[requiredProperty] === undefined) { | ||
throw new Error("".concat(requiredProperty, " is missing from ioredis cache options")); | ||
} | ||
}); | ||
}; | ||
FronteggContext.prototype.validateRedisOptions = function (redisOptions) { | ||
package_loader_1.PackageUtils.loadPackage('redis'); | ||
var requiredProperties = ['url']; | ||
requiredProperties.forEach(function (requiredProperty) { | ||
if (redisOptions[requiredProperty] === undefined) { | ||
throw new Error("".concat(requiredProperty, " is missing from redis cache options")); | ||
} | ||
}); | ||
}; | ||
return FronteggContext; | ||
@@ -26,0 +67,0 @@ }()); |
@@ -6,29 +6,2 @@ import { Request, Response } from 'express'; | ||
} | ||
export declare enum tokenTypes { | ||
UserApiToken = "userApiToken", | ||
TenantApiToken = "tenantApiToken", | ||
UserToken = "userToken" | ||
} | ||
export declare type Role = string; | ||
export declare type Permission = string; | ||
export interface IUser { | ||
id?: string; | ||
sub: string; | ||
tenantId: string; | ||
roles: Role[]; | ||
permissions: Permission[]; | ||
metadata: Record<string, any>; | ||
createdByUserId: string; | ||
type: tokenTypes; | ||
name?: string; | ||
email?: string; | ||
email_verified?: boolean; | ||
invisible?: true; | ||
tenantIds?: string[]; | ||
profilePictureUrl?: string; | ||
} | ||
export interface StatusCodeError { | ||
statusCode: number; | ||
message: string; | ||
} | ||
export declare function withAuthentication({ roles, permissions }?: IWithAuthenticationOptions): (req: Request, res: Response, next: any) => Promise<any>; |
@@ -50,11 +50,6 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.withAuthentication = exports.tokenTypes = void 0; | ||
exports.withAuthentication = void 0; | ||
var clients_1 = require("../clients"); | ||
var logger_1 = require("../components/logger"); | ||
var tokenTypes; | ||
(function (tokenTypes) { | ||
tokenTypes["UserApiToken"] = "userApiToken"; | ||
tokenTypes["TenantApiToken"] = "tenantApiToken"; | ||
tokenTypes["UserToken"] = "userToken"; | ||
})(tokenTypes = exports.tokenTypes || (exports.tokenTypes = {})); | ||
var types_1 = require("../clients/identity/types"); | ||
function withAuthentication(_a) { | ||
@@ -64,15 +59,15 @@ var _this = this; | ||
return function (req, res, next) { return __awaiter(_this, void 0, void 0, function () { | ||
var authorizationHeader, token, user, e_1, _a, statusCode, message; | ||
var authHeader, token, type, user, e_1, _a, statusCode, message; | ||
return __generator(this, function (_b) { | ||
switch (_b.label) { | ||
case 0: | ||
authorizationHeader = req.header('authorization'); | ||
if (!authorizationHeader) { | ||
authHeader = getAuthHeader(req); | ||
if (!authHeader) { | ||
return [2 /*return*/, res.status(401).send('Unauthenticated')]; | ||
} | ||
token = authorizationHeader.replace('Bearer ', ''); | ||
token = authHeader.token, type = authHeader.type; | ||
_b.label = 1; | ||
case 1: | ||
_b.trys.push([1, 3, , 4]); | ||
return [4 /*yield*/, clients_1.IdentityClient.getInstance().validateIdentityOnToken(token, { roles: roles, permissions: permissions })]; | ||
return [4 /*yield*/, clients_1.IdentityClient.getInstance().validateIdentityOnToken(token, { roles: roles, permissions: permissions }, type)]; | ||
case 2: | ||
@@ -92,10 +87,12 @@ user = _b.sent(); | ||
}; | ||
switch (req.frontegg.user.type) { | ||
case tokenTypes.UserToken: | ||
switch (user.type) { | ||
case types_1.tokenTypes.UserToken: | ||
// The subject of the token (OpenID token) is saved on the req.frontegg.user as well for easier readability | ||
req.frontegg.user.id = user.sub; | ||
break; | ||
case tokenTypes.UserApiToken: | ||
case types_1.tokenTypes.UserApiToken: | ||
req.frontegg.user.id = user.createdByUserId; | ||
break; | ||
case types_1.tokenTypes.UserAccessToken: | ||
req.frontegg.user.id = user.userId; | ||
} | ||
@@ -110,2 +107,13 @@ // And move to the next handler | ||
exports.withAuthentication = withAuthentication; | ||
function getAuthHeader(req) { | ||
var token = req.header('authorization'); | ||
if (token) { | ||
return { token: token.replace('Bearer ', ''), type: types_1.AuthHeaderType.JWT }; | ||
} | ||
token = req.header('x-api-key'); | ||
if (token) { | ||
return { token: token, type: types_1.AuthHeaderType.AccessToken }; | ||
} | ||
return null; | ||
} | ||
//# sourceMappingURL=with-authentication.js.map |
@@ -0,1 +1,8 @@ | ||
# [4.2.0-alpha.3](https://github.com/frontegg/nodejs-sdk/compare/4.2.0-alpha.2...4.2.0-alpha.3) (2023-01-22) | ||
### Features | ||
* **access-tokens:** support access tokens ([#133](https://github.com/frontegg/nodejs-sdk/issues/133)) ([7911b81](https://github.com/frontegg/nodejs-sdk/commit/7911b81ac165a4dc91dbf5ff93e5f86a5464a960)) | ||
# [4.2.0-alpha.2](https://github.com/frontegg/nodejs-sdk/compare/4.2.0-alpha.1...4.2.0-alpha.2) (2023-01-22) | ||
@@ -2,0 +9,0 @@ |
{ | ||
"name": "@frontegg/client", | ||
"version": "4.2.0-alpha.2", | ||
"version": "4.2.0-alpha.3", | ||
"description": "Frontegg Javascript Library for backend", | ||
@@ -9,2 +9,3 @@ "main": "dist/index.js", | ||
"build": "rm -rf dist && tsc", | ||
"build:watch": "rm -rf dist && tsc --watch", | ||
"lint": "eslint --ignore-path .eslintignore --ext .js,.ts .", | ||
@@ -28,4 +29,17 @@ "format": "prettier --write \"**/*.+(js|ts|json)\"", | ||
"jsonwebtoken": "^9.0.0", | ||
"node-cache": "^5.1.2", | ||
"winston": "^3.8.2" | ||
}, | ||
"peerDependencies": { | ||
"ioredis": "^5.0.0", | ||
"redis": "^4.0.0" | ||
}, | ||
"peerDependenciesMeta": { | ||
"ioredis": { | ||
"optional": true | ||
}, | ||
"redis": { | ||
"optional": true | ||
} | ||
}, | ||
"devDependencies": { | ||
@@ -45,2 +59,4 @@ "@semantic-release/changelog": "^6.0.1", | ||
"express": "^4.18.1", | ||
"ioredis": "^5.2.5", | ||
"ioredis-mock": "^8.2.2", | ||
"jest": "^28.1.3", | ||
@@ -47,0 +63,0 @@ "jest-environment-node": "^26.6.2", |
154
README.md
@@ -29,2 +29,3 @@ <br /> | ||
### Version 3.0.0 is Deprecated. Please use versions 4.x.x | ||
### If you are upgrading from version 2.x.x skip version 3.0.0 by installing the latest version | ||
@@ -37,3 +38,4 @@ | ||
### As of version 3.0.0 and 4.0.0, we will no longer provide proxy middlewares | ||
To see an example implementation of a proxy, head over to our | ||
To see an example implementation, head over to our | ||
<a href="https://github.com/frontegg-samples/nodejs-proxy-sample">sample proxy project</a> | ||
@@ -52,2 +54,3 @@ | ||
--- | ||
## Usage | ||
@@ -58,2 +61,3 @@ | ||
Initialize the frontegg context when initializing your app | ||
```javascript | ||
@@ -63,6 +67,7 @@ const { FronteggContext } = require('@frontegg/client'); | ||
FronteggContext.init({ | ||
FRONTEGG_CLIENT_ID: '<YOUR_CLIENT_ID>', | ||
FRONTEGG_API_KEY: '<YOUR_API_KEY>', | ||
FRONTEGG_CLIENT_ID: '<YOUR_CLIENT_ID>', | ||
FRONTEGG_API_KEY: '<YOUR_API_KEY>', | ||
}); | ||
``` | ||
### Middleware | ||
@@ -73,2 +78,3 @@ | ||
A simple usage example: | ||
```javascript | ||
@@ -79,9 +85,73 @@ const { withAuthentication } = require('@frontegg/client'); | ||
app.use('/protected', withAuthentication(), (req, res) => { | ||
// Authenticated user data will be available on the req.frontegg object | ||
callSomeAction(req.frontegg.user) | ||
res.status(200); | ||
// Authenticated user data will be available on the req.frontegg object | ||
callSomeAction(req.frontegg.user); | ||
res.status(200); | ||
}); | ||
``` | ||
Head over to the <a href="https://docs.frontegg.com/docs/using-frontegg-sdk">Docs</a> to find more usage examples of the guard. | ||
### Access tokens | ||
When using M2M authentication, access tokens will be cached by the SDK. | ||
By default access tokens will be cached locally, however you can use two other kinds of cache: | ||
- ioredis | ||
- redis | ||
#### Use ioredis as your cache | ||
When initializing your context, pass an access tokens options object with your ioredis parameters | ||
```javascript | ||
const { FronteggContext } = require('@frontegg/client'); | ||
const accessTokensOptions = { | ||
cache: { | ||
type: 'ioredis', | ||
options: { | ||
host: 'localhost', | ||
port: 6379, | ||
password: '', | ||
db: 10, | ||
}, | ||
}, | ||
}; | ||
FronteggContext.init( | ||
{ | ||
FRONTEGG_CLIENT_ID: '<YOUR_CLIENT_ID>', | ||
FRONTEGG_API_KEY: '<YOUR_API_KEY>', | ||
}, | ||
{ | ||
accessTokensOptions, | ||
}, | ||
); | ||
``` | ||
#### Use redis as your cache | ||
When initializing your context, pass an access tokens options object with your redis parameters | ||
```javascript | ||
const { FronteggContext } = require('@frontegg/client'); | ||
const accessTokensOptions = { | ||
cache: { | ||
type: 'redis', | ||
options: { | ||
url: 'redis[s]://[[username][:password]@][host][:port][/db-number]', | ||
}, | ||
}, | ||
}; | ||
FronteggContext.init( | ||
{ | ||
FRONTEGG_CLIENT_ID: '<YOUR_CLIENT_ID>', | ||
FRONTEGG_API_KEY: '<YOUR_API_KEY>', | ||
}, | ||
{ | ||
accessTokensOptions, | ||
}, | ||
); | ||
``` | ||
### Clients | ||
@@ -97,3 +167,3 @@ | ||
const { AuditsClient } = require('@frontegg/client'); | ||
const audits = new AuditsClient() | ||
const audits = new AuditsClient(); | ||
@@ -108,9 +178,9 @@ // initialize the module | ||
await audits.sendAudit({ | ||
tenantId: 'my-tenant-id', | ||
time: Date(), | ||
user: 'info@frontegg.com', | ||
resource: 'Portal', | ||
action: 'Login', | ||
severity: 'Medium', | ||
ip: '1.2.3.4' | ||
tenantId: 'my-tenant-id', | ||
time: Date(), | ||
user: 'info@frontegg.com', | ||
resource: 'Portal', | ||
action: 'Login', | ||
severity: 'Medium', | ||
ip: '1.2.3.4', | ||
}); | ||
@@ -123,8 +193,8 @@ ``` | ||
const { data, total } = await audits.getAudits({ | ||
tenantId: 'my-tenant-id', | ||
filter: 'any-text-filter', | ||
sortBy: 'my-sort-field', | ||
sortDirection: 'asc | desc', | ||
offset: 0, // Offset for starting the page | ||
count: 50 // Number of desired items | ||
tenantId: 'my-tenant-id', | ||
filter: 'any-text-filter', | ||
sortBy: 'my-sort-field', | ||
sortDirection: 'asc | desc', | ||
offset: 0, // Offset for starting the page | ||
count: 50, // Number of desired items | ||
}); | ||
@@ -141,3 +211,3 @@ ``` | ||
const authenticator = new FronteggAuthenticator(); | ||
await authenticator.init('<YOUR_CLIENT_ID>', '<YOUR_API_KEY>') | ||
await authenticator.init('<YOUR_CLIENT_ID>', '<YOUR_API_KEY>'); | ||
@@ -147,15 +217,21 @@ // You can optionally set the base url from the HttpClient | ||
await httpClient.post('identity/resources/auth/v1/user', { | ||
await httpClient.post( | ||
'identity/resources/auth/v1/user', | ||
{ | ||
email: 'johndoe@acme.com', | ||
password: 'my-super-duper-password' | ||
}, { | ||
password: 'my-super-duper-password', | ||
}, | ||
{ | ||
// When providing vendor-host, it will replace(<...>) https://<api>.frontegg.com with vendor host | ||
'frontegg-vendor-host': 'acme.frontegg' | ||
}); | ||
'frontegg-vendor-host': 'acme.frontegg', | ||
}, | ||
); | ||
``` | ||
### Validating JWT manually | ||
If required you can implement your own middleware which will validate the Frontegg JWT using the `IdentityClient` | ||
First, let's import the `IdentityClient` | ||
```javascript | ||
@@ -166,2 +242,3 @@ const { IdentityClient } = require('@frontegg/client'); | ||
Then, initialize the client | ||
```javascript | ||
@@ -172,17 +249,18 @@ const identityClient = new IdentityClient({ FRONTEGG_CLIENT_ID: 'your-client-id', FRONTEGG_API_KEY: 'your-api-key' }); | ||
And use this client to validate | ||
```javascript | ||
app.use('/protected', (req, res, next) => { | ||
const token = req.headers.authorization; | ||
let user: IUser; | ||
try { | ||
user = identityClient.validateIdentityOnToken(token, { roles: ['admin'], permissions: ['read'] }); | ||
req.user = user; | ||
} catch (e) { | ||
console.error(e); | ||
next(e); | ||
return; | ||
} | ||
next(); | ||
const token = req.headers.authorization; | ||
let user: IUser; | ||
try { | ||
user = identityClient.validateIdentityOnToken(token, { roles: ['admin'], permissions: ['read'] }); | ||
req.user = user; | ||
} catch (e) { | ||
console.error(e); | ||
next(e); | ||
return; | ||
} | ||
next(); | ||
}); | ||
``` |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
228852
155
3275
253
7
22
11
+ Addednode-cache@^5.1.2
+ Added@ioredis/commands@1.2.0(transitive)
+ Added@redis/bloom@1.2.0(transitive)
+ Added@redis/client@1.6.0(transitive)
+ Added@redis/graph@1.1.1(transitive)
+ Added@redis/json@1.0.7(transitive)
+ Added@redis/search@1.2.0(transitive)
+ Added@redis/time-series@1.1.0(transitive)
+ Addedclone@2.1.2(transitive)
+ Addedcluster-key-slot@1.1.2(transitive)
+ Addeddebug@4.4.0(transitive)
+ Addeddenque@2.1.0(transitive)
+ Addedgeneric-pool@3.9.0(transitive)
+ Addedioredis@5.4.2(transitive)
+ Addedlodash.defaults@4.2.0(transitive)
+ Addedlodash.isarguments@3.1.0(transitive)
+ Addednode-cache@5.1.2(transitive)
+ Addedredis@4.7.0(transitive)
+ Addedredis-errors@1.2.0(transitive)
+ Addedredis-parser@3.0.0(transitive)
+ Addedstandard-as-callback@2.1.0(transitive)
+ Addedyallist@4.0.0(transitive)