node-easywechat 3.1.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,18 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v3.1.2 (2022-12-06)
5
+
6
+ - Feat: 新作微信支付模块的文件上传方法
7
+
8
+ - Fix: nodejs版本不能低于15.6.0
9
+ - Fix: 更新依赖的nodejs版本
10
+ - Fix: 修复RSA证书使用格式不正确导致的签名异常问题
11
+
12
+ ## v3.1.1 (2022-11-15)
13
+
14
+ - Feat: 增加请求失败时的重试机制,具体见配置项http.retry
15
+
4
16
  ## v3.1.0 (2022-11-09)
5
17
 
6
18
  - Feat: 新增开放平台模块
@@ -9,6 +21,8 @@
9
21
  - Fix: 修复配置项类型判断异常的问题
10
22
  - Fix: 修复请求客户端无法设置请求是否错误判断逻辑的方法
11
23
 
24
+ - Docs: 增加开放平台的说明
25
+
12
26
  - Perf: 优化url路径解析方法
13
27
 
14
28
  ## v3.0.1 (2022-08-26)
package/README.md CHANGED
@@ -16,6 +16,8 @@
16
16
 
17
17
  `npm install -S node-easywechat@next`
18
18
 
19
+ **注:3.x 版本需要 Node.js 15.6.0 及以上版本**
20
+
19
21
  ### 使用说明
20
22
 
21
23
  绝大部分API都可以根据 [EasyWechat 的文档](https://www.easywechat.com/5.x/) 来使用。小部分(如获取请求相关数据、返回响应数据、支付证书等)的操作,由于语言环境的不同,会有不同处理。具体可以查看 [node-easywechat-demo](https://github.com/hpyer/node-easywechat-demo/) 以及下方的[自定义模块说明](#自定义模块模块替换使用方法) 。如果仍有疑问,请提issue,谢谢~
@@ -78,7 +80,11 @@ let data = response.toObject();
78
80
  {
79
81
  // axios 请求参数
80
82
  // 详见:https://github.com/axios/axios#request-config
81
- http: {},
83
+ http: {
84
+ // 请求失败时,自动重试。默认不重试
85
+ // 详见:https://github.com/softonic/axios-retry#options
86
+ retry: {}
87
+ },
82
88
 
83
89
  // 缓存以文件(默认设置)存储时,需要的配置项
84
90
  file_cache: {
@@ -1,7 +1,7 @@
1
1
  import { AxiosInstance, AxiosRequestConfig, Method } from 'axios';
2
2
  import HttpClientInterface from './Contracts/HttpClientInterface';
3
3
  import HttpClientResponse from './HttpClientResponse';
4
- import { HttpClientFailureJudgeClosure, LogHandler } from '../../Types/global';
4
+ import { HttpClientFailureJudgeClosure, HttpConfig, LogHandler } from '../../Types/global';
5
5
  import FormData from 'form-data';
6
6
  declare class HttpClient implements HttpClientInterface {
7
7
  protected axios: AxiosInstance;
@@ -28,6 +28,6 @@ declare class HttpClient implements HttpClientInterface {
28
28
  * @param config
29
29
  * @returns
30
30
  */
31
- static create(config?: AxiosRequestConfig, failureJudge?: HttpClientFailureJudgeClosure, throwError?: boolean): HttpClient;
31
+ static create(config?: HttpConfig, failureJudge?: HttpClientFailureJudgeClosure, throwError?: boolean): HttpClient;
32
32
  }
33
33
  export = HttpClient;
@@ -15,6 +15,7 @@ const axios_1 = __importDefault(require("axios"));
15
15
  const HttpClientResponse_1 = __importDefault(require("./HttpClientResponse"));
16
16
  const Utils_1 = require("../Support/Utils");
17
17
  const form_data_1 = __importDefault(require("form-data"));
18
+ const axios_retry_1 = __importDefault(require("axios-retry"));
18
19
  class HttpClient {
19
20
  constructor(axios, failureJudge = null, throwError = false) {
20
21
  this.axios = axios;
@@ -78,8 +79,13 @@ class HttpClient {
78
79
  }
79
80
  if (options['formData'] && Object.keys(options['formData']).length > 0) {
80
81
  let formData = new form_data_1.default();
81
- for (let key in options['formData']) {
82
- formData.append(key, options['formData'][key]);
82
+ if (options['formData'] instanceof form_data_1.default) {
83
+ formData = options['formData'];
84
+ }
85
+ else {
86
+ for (let key in options['formData']) {
87
+ formData.append(key, options['formData'][key]);
88
+ }
83
89
  }
84
90
  if (options.data)
85
91
  for (let key in options.data) {
@@ -139,7 +145,11 @@ class HttpClient {
139
145
  * @returns
140
146
  */
141
147
  static create(config = null, failureJudge = null, throwError = false) {
142
- return new HttpClient(axios_1.default.create(config), failureJudge, throwError);
148
+ let axios = axios_1.default.create(config);
149
+ if (config && config.retry) {
150
+ (0, axios_retry_1.default)(axios, config.retry);
151
+ }
152
+ return new HttpClient(axios, failureJudge, throwError);
143
153
  }
144
154
  }
145
155
  module.exports = HttpClient;
@@ -1,12 +1,13 @@
1
+ /// <reference types="node" />
1
2
  export declare class PrivateKey {
2
- protected key: string;
3
3
  protected passphrase?: string;
4
+ protected key: Buffer;
4
5
  constructor(key: string, passphrase?: string);
5
6
  /**
6
- * 获取私钥
7
+ * 获取私钥内容
7
8
  * @returns
8
9
  */
9
- getKey(): string;
10
+ getKey(): Buffer;
10
11
  /**
11
12
  * 获取密码
12
13
  * @returns
@@ -16,5 +17,5 @@ export declare class PrivateKey {
16
17
  * 转为字符串
17
18
  * @returns
18
19
  */
19
- toString(): string;
20
+ toString(): Buffer;
20
21
  }
@@ -7,14 +7,13 @@ exports.PrivateKey = void 0;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  class PrivateKey {
9
9
  constructor(key, passphrase) {
10
- this.key = key;
11
10
  this.passphrase = passphrase;
12
11
  if (fs_1.default.existsSync(key)) {
13
- this.key = fs_1.default.readFileSync(key, 'utf8') || '';
12
+ this.key = fs_1.default.readFileSync(key) || Buffer.from('');
14
13
  }
15
14
  }
16
15
  /**
17
- * 获取私钥
16
+ * 获取私钥内容
18
17
  * @returns
19
18
  */
20
19
  getKey() {
@@ -1,11 +1,17 @@
1
+ /// <reference types="node" />
1
2
  export declare class PublicKey {
2
- certificate: string;
3
+ protected certificate: Buffer;
3
4
  constructor(certificate: string);
4
5
  /**
5
6
  * 获取公钥的序列号
6
7
  * @returns
7
8
  */
8
9
  getSerialNo(): string;
10
+ /**
11
+ * 获取证书内容
12
+ * @returns
13
+ */
14
+ getValue(): Buffer;
9
15
  /**
10
16
  * 转为字符串
11
17
  * @returns
@@ -8,9 +8,8 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const crypto_1 = require("crypto");
9
9
  class PublicKey {
10
10
  constructor(certificate) {
11
- this.certificate = certificate;
12
11
  if (fs_1.default.existsSync(certificate)) {
13
- this.certificate = fs_1.default.readFileSync(certificate, 'utf8') || '';
12
+ this.certificate = fs_1.default.readFileSync(certificate) || Buffer.from('');
14
13
  }
15
14
  }
16
15
  /**
@@ -25,12 +24,19 @@ class PublicKey {
25
24
  throw new Error('Read the $certificate failed, please check it whether or nor correct');
26
25
  }
27
26
  }
27
+ /**
28
+ * 获取证书内容
29
+ * @returns
30
+ */
31
+ getValue() {
32
+ return this.certificate;
33
+ }
28
34
  /**
29
35
  * 转为字符串
30
36
  * @returns
31
37
  */
32
38
  toString() {
33
- return this.certificate;
39
+ return this.certificate.toString();
34
40
  }
35
41
  }
36
42
  exports.PublicKey = PublicKey;
@@ -1,11 +1,12 @@
1
+ import Crypto from 'crypto';
1
2
  import Stream from 'stream';
2
- export declare const createHash: (str: string, type?: string) => any;
3
- export declare const createHmac: (str: string, key: string, type?: string) => any;
3
+ export declare const createHash: (str: Crypto.BinaryLike, type?: string, target?: Crypto.BinaryToTextEncoding) => any;
4
+ export declare const createHmac: (str: Crypto.BinaryLike, key: Crypto.BinaryLike, type?: string, target?: Crypto.BinaryToTextEncoding) => any;
4
5
  /**
5
- * 计算文件的 md5 值
6
+ * 计算文件的哈希值
6
7
  * @param path 文件路径或文件可读流
7
8
  */
8
- export declare const md5File: (path: string | Stream.Readable) => Promise<string>;
9
+ export declare const createFileHash: (path: string | Stream.Readable, type?: string, target?: Crypto.BinaryToTextEncoding) => Promise<string>;
9
10
  export declare const getTimestamp: (datetime?: string) => number;
10
11
  export declare const buildQueryString: (data: Record<string, any>, options?: Record<string, any>) => string;
11
12
  export declare const parseQueryString: (data: string, options?: Record<string, any>) => Record<string, any>;
@@ -12,26 +12,26 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.createUserAgent = exports.buildXml = exports.parseXml = exports.singleItem = exports.strSnake = exports.strCamel = exports.strStudly = exports.strLcwords = exports.strUcwords = exports.rtrim = exports.ltrim = exports.trim = exports.applyMixins = exports.inArray = exports.isIp = exports.isIpv6 = exports.isIpv4 = exports.isFunction = exports.isObject = exports.isNumber = exports.isArray = exports.isString = exports.makeSignature = exports.randomString = exports.parseQueryString = exports.buildQueryString = exports.getTimestamp = exports.md5File = exports.createHmac = exports.createHash = void 0;
15
+ exports.createUserAgent = exports.buildXml = exports.parseXml = exports.singleItem = exports.strSnake = exports.strCamel = exports.strStudly = exports.strLcwords = exports.strUcwords = exports.rtrim = exports.ltrim = exports.trim = exports.applyMixins = exports.inArray = exports.isIp = exports.isIpv6 = exports.isIpv4 = exports.isFunction = exports.isObject = exports.isNumber = exports.isArray = exports.isString = exports.makeSignature = exports.randomString = exports.parseQueryString = exports.buildQueryString = exports.getTimestamp = exports.createFileHash = exports.createHmac = exports.createHash = void 0;
16
16
  const crypto_1 = __importDefault(require("crypto"));
17
17
  const qs_1 = __importDefault(require("qs"));
18
18
  const xml2js_1 = __importDefault(require("xml2js"));
19
19
  const stream_1 = __importDefault(require("stream"));
20
20
  const fs_1 = __importDefault(require("fs"));
21
21
  const axios_1 = __importDefault(require("axios"));
22
- const createHash = function (str, type = 'sha1') {
23
- return crypto_1.default.createHash(type).update(str).digest('hex');
22
+ const createHash = function (str, type = 'sha1', target = 'hex') {
23
+ return crypto_1.default.createHash(type).update(str).digest(target);
24
24
  };
25
25
  exports.createHash = createHash;
26
- const createHmac = function (str, key, type = 'sha256') {
27
- return crypto_1.default.createHmac(type, key).update(str).digest('hex');
26
+ const createHmac = function (str, key, type = 'sha256', target = 'hex') {
27
+ return crypto_1.default.createHmac(type, key).update(str).digest(target);
28
28
  };
29
29
  exports.createHmac = createHmac;
30
30
  /**
31
- * 计算文件的 md5 值
31
+ * 计算文件的哈希值
32
32
  * @param path 文件路径或文件可读流
33
33
  */
34
- const md5File = function (path) {
34
+ const createFileHash = function (path, type = 'sha1', target = 'hex') {
35
35
  return new Promise((reslove, reject) => {
36
36
  let stream;
37
37
  if ((0, exports.isString)(path)) {
@@ -41,17 +41,17 @@ const md5File = function (path) {
41
41
  stream = new stream_1.default.PassThrough();
42
42
  path.pipe(stream);
43
43
  }
44
- let md5sum = crypto_1.default.createHash('md5');
44
+ let md5sum = crypto_1.default.createHash(type);
45
45
  stream.on('data', function (chunk) {
46
46
  md5sum.update(chunk);
47
47
  });
48
48
  stream.on('end', function () {
49
- let str = md5sum.digest('hex').toUpperCase();
49
+ let str = md5sum.digest(target).toUpperCase();
50
50
  reslove(str);
51
51
  });
52
52
  });
53
53
  };
54
- exports.md5File = md5File;
54
+ exports.createFileHash = createFileHash;
55
55
  const getTimestamp = function (datetime = null) {
56
56
  let time;
57
57
  try {
@@ -13,8 +13,6 @@ const Merchant_1 = __importDefault(require("./Merchant"));
13
13
  const Server_1 = __importDefault(require("./Server"));
14
14
  const Utils_2 = __importDefault(require("./Utils"));
15
15
  const Config_1 = __importDefault(require("../OfficialAccount/Config"));
16
- const PrivateKey_1 = require("../Core/Support/PrivateKey");
17
- const PublicKey_1 = require("../Core/Support/PublicKey");
18
16
  const Client_1 = __importDefault(require("./Client"));
19
17
  /**
20
18
  * 微信支付应用
@@ -34,7 +32,7 @@ class Application {
34
32
  getMerchant() {
35
33
  var _a;
36
34
  if (!this.merchant) {
37
- this.merchant = new Merchant_1.default(this.config.get('mch_id'), new PrivateKey_1.PrivateKey(this.config.get('private_key')), new PublicKey_1.PublicKey(this.config.get('certificate')), this.config.get('secret_key'), this.config.get('v2_secret_key'), (_a = this.config.get('platform_certs')) !== null && _a !== void 0 ? _a : []);
35
+ this.merchant = new Merchant_1.default(this.config.get('mch_id'), this.config.get('private_key'), this.config.get('certificate'), this.config.get('secret_key'), this.config.get('v2_secret_key'), (_a = this.config.get('platform_certs')) !== null && _a !== void 0 ? _a : []);
38
36
  }
39
37
  return this.merchant;
40
38
  }
@@ -1,3 +1,5 @@
1
+ /// <reference types="node" />
2
+ import fs from 'fs';
1
3
  import { Method, AxiosRequestConfig, AxiosInstance } from "axios";
2
4
  import HttpClientInterface from "../Core/HttpClient/Contracts/HttpClientInterface";
3
5
  import HttpClientMethodsMixin from '../Core/HttpClient/Mixins/HttpClientMethodsMixin';
@@ -16,6 +18,16 @@ declare class Client implements HttpClientInterface {
16
18
  setInstance(instance: AxiosInstance): this;
17
19
  setLogger(logger: LogHandler): this;
18
20
  request(method: Method, url: string, payload?: AxiosRequestConfig<any>): Promise<HttpClientResponse>;
21
+ /**
22
+ * 文件上传
23
+ * @see https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter2_1_1.shtml
24
+ * @param uri 接口地址
25
+ * @param file 文件路径或文件可读流
26
+ * @param meta 文件元信息,包含 filename 和 sha256 两个字段
27
+ * @param filename 文件名,必须以 .jpg、.bmp、.png 为后缀
28
+ * @returns
29
+ */
30
+ uploadMedia(uri: string, file: string | fs.ReadStream, meta?: Record<string, any>, filename?: string): Promise<HttpClientResponse>;
19
31
  /**
20
32
  * 判断是否是V3请求
21
33
  * @param url 请求地址
@@ -11,6 +11,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
+ const fs_1 = __importDefault(require("fs"));
14
15
  const merge_1 = __importDefault(require("merge"));
15
16
  const Utils_1 = require("../Core/Support/Utils");
16
17
  const HttpClient_1 = __importDefault(require("../Core/HttpClient/HttpClient"));
@@ -18,6 +19,7 @@ const HttpClientMethodsMixin_1 = __importDefault(require("../Core/HttpClient/Mix
18
19
  const PresetMixin_1 = __importDefault(require("../Core/HttpClient/Mixins/PresetMixin"));
19
20
  const Signature_1 = __importDefault(require("./Signature"));
20
21
  const LegacySignature_1 = __importDefault(require("./LegacySignature"));
22
+ const form_data_1 = __importDefault(require("form-data"));
21
23
  class Client {
22
24
  constructor(merchant, client, defaultOptions = {}) {
23
25
  var _a;
@@ -89,6 +91,42 @@ class Client {
89
91
  return this.client.request(method, (0, Utils_1.ltrim)(url, '\\/+'), payload);
90
92
  });
91
93
  }
94
+ /**
95
+ * 文件上传
96
+ * @see https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter2_1_1.shtml
97
+ * @param uri 接口地址
98
+ * @param file 文件路径或文件可读流
99
+ * @param meta 文件元信息,包含 filename 和 sha256 两个字段
100
+ * @param filename 文件名,必须以 .jpg、.bmp、.png 为后缀
101
+ * @returns
102
+ */
103
+ uploadMedia(uri, file, meta = null, filename = null) {
104
+ return __awaiter(this, void 0, void 0, function* () {
105
+ if (typeof file === 'string') {
106
+ file = fs_1.default.createReadStream(file);
107
+ }
108
+ if (!meta) {
109
+ meta = {
110
+ filename: filename !== null && filename !== void 0 ? filename : 'file.jpg',
111
+ sha256: yield (0, Utils_1.createFileHash)(file, 'sha256'),
112
+ };
113
+ }
114
+ let metaJson = JSON.stringify(meta);
115
+ let formData = new form_data_1.default();
116
+ formData.append('file', file);
117
+ formData.append('meta', metaJson, {
118
+ contentType: 'application/json',
119
+ });
120
+ return this.client.request('POST', (0, Utils_1.ltrim)(uri, '\\/+'), {
121
+ formData,
122
+ headers: {
123
+ 'authorization': this.createSignature('POST', uri, {
124
+ data: metaJson,
125
+ }),
126
+ }
127
+ });
128
+ });
129
+ }
92
130
  /**
93
131
  * 判断是否是V3请求
94
132
  * @param url 请求地址
@@ -3,12 +3,12 @@ import { PublicKey } from "../Core/Support/PublicKey";
3
3
  import MerchantInterface from "./Contracts/MerchantInterface";
4
4
  declare class Merchant implements MerchantInterface {
5
5
  protected mchId: string;
6
- protected privateKey: PrivateKey;
7
- protected certificate: PublicKey;
8
6
  protected secretKey: string;
9
7
  protected v2SecretKey: string;
10
8
  protected platformCerts: Record<string, PublicKey>;
11
- constructor(mchId: string, privateKey: PrivateKey, certificate: PublicKey, secretKey: string, v2SecretKey?: string, platformCerts?: Record<string, string | PublicKey> | string[] | PublicKey[]);
9
+ protected privateKey: PrivateKey;
10
+ protected certificate: PublicKey;
11
+ constructor(mchId: string, privateKey: string | PrivateKey, certificate: string | PublicKey, secretKey: string, v2SecretKey?: string, platformCerts?: Record<string, string | PublicKey> | string[] | PublicKey[]);
12
12
  /**
13
13
  * 统一规范化平台证书
14
14
  * @param platformCerts 平台证书列表
@@ -1,13 +1,24 @@
1
1
  'use strict';
2
+ const PrivateKey_1 = require("../Core/Support/PrivateKey");
2
3
  const PublicKey_1 = require("../Core/Support/PublicKey");
3
4
  class Merchant {
4
5
  constructor(mchId, privateKey, certificate, secretKey, v2SecretKey = null, platformCerts = []) {
5
6
  this.mchId = mchId;
6
- this.privateKey = privateKey;
7
- this.certificate = certificate;
8
7
  this.secretKey = secretKey;
9
8
  this.v2SecretKey = v2SecretKey;
10
9
  this.platformCerts = {};
10
+ if (!(privateKey instanceof PrivateKey_1.PrivateKey)) {
11
+ this.privateKey = new PrivateKey_1.PrivateKey(privateKey);
12
+ }
13
+ else {
14
+ this.privateKey = privateKey;
15
+ }
16
+ if (!(certificate instanceof PublicKey_1.PublicKey)) {
17
+ this.certificate = new PublicKey_1.PublicKey(certificate);
18
+ }
19
+ else {
20
+ this.certificate = certificate;
21
+ }
11
22
  this.platformCerts = this.normalizePlatformCerts(platformCerts);
12
23
  }
13
24
  /**
@@ -36,8 +36,8 @@ class Signature {
36
36
  }
37
37
  let signString = `${method.toUpperCase()}\n${pathname}\n${timestamp}\n${nonce}\n${body}`;
38
38
  let rsa = new RSA_1.default;
39
- rsa.setPublicKey(this.merchant.getCertificate().toString());
40
- rsa.setPrivateKey(this.merchant.getPrivateKey().toString());
39
+ rsa.setPublicKey(this.merchant.getCertificate().getValue());
40
+ rsa.setPrivateKey(this.merchant.getPrivateKey().getKey());
41
41
  let sign = rsa.sign(signString);
42
42
  return `WECHATPAY2-SHA256-RSA2048 mchid="${this.merchant.getMerchantId()}",nonce_str="${nonce}",timestamp="${timestamp}",serial_no="${this.merchant.getCertificate().getSerialNo()}",signature="${sign}"`;
43
43
  }
@@ -74,6 +74,35 @@ export declare interface CacheFileConfig {
74
74
  ext: string
75
75
  }
76
76
 
77
+ /**
78
+ * 网络请求配置
79
+ */
80
+ export declare interface HttpConfig extends AxiosRequestConfig {
81
+ /**
82
+ * 是否抛出异常
83
+ */
84
+ throw?: boolean;
85
+ /**
86
+ * 是否抛出异常
87
+ * @see https://github.com/softonic/axios-retry#options
88
+ */
89
+ retry?: IAxiosRetry.IAxiosRetryConfig;
90
+ // retry?: {
91
+ // /**
92
+ // * 仅以下状态码重试
93
+ // */
94
+ // http_codes: number[];
95
+ // /**
96
+ // * 最大重试次数
97
+ // */
98
+ // max_retries: number;
99
+ // /**
100
+ // * 请求间隔 (毫秒)
101
+ // */
102
+ // delay: number;
103
+ // };
104
+ }
105
+
77
106
  /**
78
107
  * 基础配置
79
108
  */
@@ -81,7 +110,7 @@ export declare interface BaseConfig {
81
110
  /**
82
111
  * 网络请求相关配置
83
112
  */
84
- http?: AxiosRequestConfig;
113
+ http?: HttpConfig;
85
114
 
86
115
  /**
87
116
  * 文件缓存相关配置
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-easywechat",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
4
4
  "description": "EasyWechat SDK for Node.js (NOT OFFICIAL)",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -18,15 +18,19 @@
18
18
  ],
19
19
  "author": "Hpyer",
20
20
  "license": "MIT",
21
+ "engines": {
22
+ "node": ">=15.6.0"
23
+ },
21
24
  "devDependencies": {
22
25
  "@types/node": "^17.0.23",
23
26
  "axios-mock-adapter": "^1.21.1",
24
27
  "mocha": "^9.2.2",
25
- "package-release": "^1.0.2",
28
+ "package-release": "^1.0.3",
26
29
  "typescript": "^4.6.3"
27
30
  },
28
31
  "dependencies": {
29
32
  "axios": "^0.26.1",
33
+ "axios-retry": "^3.3.1",
30
34
  "form-data": "^4.0.0",
31
35
  "merge": "^2.1.1",
32
36
  "node-socialite": "^1.2.5",