@joonweb/joonweb-sdk 1.0.0
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/index.js +8 -0
- package/package.json +60 -0
- package/src/Auth/Auth.js +193 -0
- package/src/Auth/OAuth.js +135 -0
- package/src/Auth/SessionManager.js +145 -0
- package/src/Auth/session/Session.js +46 -0
- package/src/Auth/session/StorageInterface.js +48 -0
- package/src/Auth/session/storage/MemoryStorage.js +34 -0
- package/src/Auth/session/storage/MongoDBStorage.js +107 -0
- package/src/Auth/session/storage/MySQLStorage.js +281 -0
- package/src/Auth/session/storage/PostgreSQLStorage.js +372 -0
- package/src/Auth/session/storage/RedisStorage.js +133 -0
- package/src/Auth/session/storage/SQLiteStorage.js +133 -0
- package/src/Auth/session/storage/index.js +46 -0
- package/src/Clients/BaseClient.js +66 -0
- package/src/Context.js +82 -0
- package/src/Helper.js +92 -0
- package/src/JoonWebAPI.js +81 -0
- package/src/Resources/BaseResource.js +32 -0
- package/src/Resources/Customer.js +34 -0
- package/src/Resources/Order.js +18 -0
- package/src/Resources/Product.js +30 -0
- package/src/Resources/Site.js +11 -0
- package/src/Resources/Theme.js +7 -0
- package/src/Resources/Webhook.js +30 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
|
|
3
|
+
class BaseClient {
|
|
4
|
+
constructor(accessToken = null, siteDomain = null) {
|
|
5
|
+
this.accessToken = accessToken;
|
|
6
|
+
this.siteDomain = siteDomain;
|
|
7
|
+
this.timeout = 30000;
|
|
8
|
+
this.apiVersion = process.env.JOONWEB_API_VERSION || '26.0';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
setAccessToken(token) {
|
|
12
|
+
this.accessToken = token;
|
|
13
|
+
return this;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
setSiteDomain(domain) {
|
|
17
|
+
this.siteDomain = domain;
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async request(endpoint, method = 'GET', data = null, params = {}) {
|
|
22
|
+
if (!this.siteDomain) {
|
|
23
|
+
throw new Error('Site domain is not set. Call setSiteDomain() first.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!this.accessToken) {
|
|
27
|
+
throw new Error('Access token is not set. Call setAccessToken() first.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const url = `https://${this.siteDomain}/api/admin/${this.apiVersion}${endpoint}`;
|
|
31
|
+
|
|
32
|
+
const config = {
|
|
33
|
+
method: method.toLowerCase(),
|
|
34
|
+
url: url,
|
|
35
|
+
headers: {
|
|
36
|
+
'Content-Type': 'application/json',
|
|
37
|
+
'Accept': 'application/json',
|
|
38
|
+
'X-JoonWeb-Access-Token': this.accessToken
|
|
39
|
+
},
|
|
40
|
+
timeout: this.timeout,
|
|
41
|
+
params: Object.keys(params).length > 0 ? params : undefined
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (data && ['post', 'put', 'patch'].includes(config.method)) {
|
|
45
|
+
config.data = data;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const response = await axios(config);
|
|
50
|
+
return response.data;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error.response) {
|
|
53
|
+
const apiError = new Error(
|
|
54
|
+
`JoonWeb API Error ${error.response.status}: ${JSON.stringify(error.response.data)}`
|
|
55
|
+
);
|
|
56
|
+
apiError.status = error.response.status;
|
|
57
|
+
apiError.data = error.response.data;
|
|
58
|
+
throw apiError;
|
|
59
|
+
} else {
|
|
60
|
+
throw new Error(`JoonWeb API Request Failed: ${error.message}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = BaseClient;
|
package/src/Context.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// src/Context.js
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class Context {
|
|
5
|
+
static API_KEY = null;
|
|
6
|
+
static API_SECRET_KEY = null;
|
|
7
|
+
static API_VERSION = '26.0';
|
|
8
|
+
static IS_EMBEDDED_APP = true;
|
|
9
|
+
static SESSION_STORAGE = null;
|
|
10
|
+
static APP_NAME = 'JoonWeb App';
|
|
11
|
+
static HOST_SCHEME = 'https';
|
|
12
|
+
static HOST_NAME = null;
|
|
13
|
+
|
|
14
|
+
// New: Storage configuration
|
|
15
|
+
static SESSION_STORAGE_TYPE = 'memory'; // 'memory', 'sqlite', 'mongodb', 'redis'
|
|
16
|
+
static SESSION_STORAGE_OPTIONS = {};
|
|
17
|
+
|
|
18
|
+
static init(options = {}) {
|
|
19
|
+
this.API_KEY = options.api_key || process.env.JOONWEB_API_KEY || this.API_KEY;
|
|
20
|
+
this.API_SECRET_KEY = options.api_secret || process.env.JOONWEB_API_SECRET || this.API_SECRET_KEY;
|
|
21
|
+
this.API_VERSION = options.api_version || process.env.JOONWEB_API_VERSION || this.API_VERSION;
|
|
22
|
+
this.IS_EMBEDDED_APP = options.is_embedded !== undefined ? options.is_embedded : this.IS_EMBEDDED_APP;
|
|
23
|
+
this.APP_NAME = options.app_name || process.env.APP_NAME || this.APP_NAME;
|
|
24
|
+
this.HOST_NAME = options.host_name || process.env.HOST_NAME || this.HOST_NAME;
|
|
25
|
+
|
|
26
|
+
// Storage configuration
|
|
27
|
+
this.SESSION_STORAGE_TYPE = options.session_storage_type ||
|
|
28
|
+
process.env.JOONWEB_SESSION_STORAGE_TYPE ||
|
|
29
|
+
'memory';
|
|
30
|
+
this.SESSION_STORAGE_OPTIONS = options.session_storage_options || {};
|
|
31
|
+
|
|
32
|
+
// Initialize session storage
|
|
33
|
+
this.initSessionStorage();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static initSessionStorage() {
|
|
37
|
+
try {
|
|
38
|
+
const { createStorageAdapter } = require('./Auth/session/storage');
|
|
39
|
+
const storageAdapter = createStorageAdapter(
|
|
40
|
+
this.SESSION_STORAGE_TYPE,
|
|
41
|
+
this.SESSION_STORAGE_OPTIONS
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const SessionManager = require('./SessionManager');
|
|
45
|
+
this.SESSION_STORAGE = new SessionManager(storageAdapter);
|
|
46
|
+
|
|
47
|
+
console.log(`✅ Session storage initialized: ${this.SESSION_STORAGE_TYPE}`);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('❌ Failed to initialize session storage:', error.message);
|
|
50
|
+
|
|
51
|
+
// Fallback to memory storage
|
|
52
|
+
try {
|
|
53
|
+
const { MemoryStorage } = require('./Auth/session/storage');
|
|
54
|
+
const SessionManager = require('./Auth/SessionManager');
|
|
55
|
+
this.SESSION_STORAGE = new SessionManager(new MemoryStorage());
|
|
56
|
+
console.log('✅ Fallback to memory storage');
|
|
57
|
+
} catch (e) {
|
|
58
|
+
this.SESSION_STORAGE = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static throwIfUninitialized() {
|
|
64
|
+
if (!this.API_KEY || !this.API_SECRET_KEY) {
|
|
65
|
+
throw new Error('JoonWeb Context not initialized. Call Context::init() with api_key and api_secret.');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static isEmbeddedApp() {
|
|
70
|
+
return this.IS_EMBEDDED_APP;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Get session storage instance
|
|
74
|
+
static getSessionStorage() {
|
|
75
|
+
if (!this.SESSION_STORAGE) {
|
|
76
|
+
throw new Error('Session storage not initialized. Call Context::init() first.');
|
|
77
|
+
}
|
|
78
|
+
return this.SESSION_STORAGE;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = Context;
|
package/src/Helper.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
class Helper {
|
|
4
|
+
/**
|
|
5
|
+
* Sanitize MyJoonWeb domain
|
|
6
|
+
*/
|
|
7
|
+
static sanitizeDomain(domain) {
|
|
8
|
+
if (!domain) return null;
|
|
9
|
+
|
|
10
|
+
domain = domain.trim();
|
|
11
|
+
|
|
12
|
+
// Remove protocol
|
|
13
|
+
domain = domain.replace(/^https?:\/\//, '');
|
|
14
|
+
|
|
15
|
+
// Remove trailing slash
|
|
16
|
+
domain = domain.replace(/\/$/, '');
|
|
17
|
+
|
|
18
|
+
// Check if it's a JoonWeb domain
|
|
19
|
+
if (domain.match(/[a-zA-Z0-9][a-zA-Z0-9\-]*\.myjoonweb\.com/) ||
|
|
20
|
+
domain.match(/[a-zA-Z0-9][a-zA-Z0-9\-]*\.joonweb\.com/)) {
|
|
21
|
+
return domain;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// If it's just a site name, append .myjoonweb.com
|
|
25
|
+
if (domain.match(/^[a-zA-Z0-9][a-zA-Z0-9\-]*$/)) {
|
|
26
|
+
return `${domain}.myjoonweb.com`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Verify HMAC
|
|
34
|
+
*/
|
|
35
|
+
static verifyHmac(params, secret) {
|
|
36
|
+
const hmac = params.hmac;
|
|
37
|
+
if (!hmac) return false;
|
|
38
|
+
|
|
39
|
+
delete params.hmac;
|
|
40
|
+
|
|
41
|
+
// Sort keys
|
|
42
|
+
const sortedParams = Object.keys(params)
|
|
43
|
+
.sort()
|
|
44
|
+
.map(key => `${key}=${params[key]}`)
|
|
45
|
+
.join('&');
|
|
46
|
+
|
|
47
|
+
const calculatedHmac = crypto
|
|
48
|
+
.createHmac('sha256', secret)
|
|
49
|
+
.update(sortedParams)
|
|
50
|
+
.digest('hex');
|
|
51
|
+
|
|
52
|
+
return crypto.timingSafeEqual(
|
|
53
|
+
Buffer.from(hmac),
|
|
54
|
+
Buffer.from(calculatedHmac)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate OAuth redirect URL
|
|
60
|
+
*/
|
|
61
|
+
static getAuthorizationUrl(site, scopes, redirectUri, apiKey, nonce = null) {
|
|
62
|
+
const state = nonce || crypto.randomBytes(16).toString('hex');
|
|
63
|
+
const scope = Array.isArray(scopes) ? scopes.join(',') : scopes;
|
|
64
|
+
|
|
65
|
+
const params = new URLSearchParams({
|
|
66
|
+
client_id: apiKey,
|
|
67
|
+
scope: scope,
|
|
68
|
+
redirect_uri: redirectUri,
|
|
69
|
+
state: state,
|
|
70
|
+
'grant_options[]': 'per-user'
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
url: `https://${site}/admin/oauth/authorize?${params.toString()}`,
|
|
75
|
+
state: state
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Decode JWT session token (for embedded apps)
|
|
81
|
+
*/
|
|
82
|
+
static decodeSessionToken(token) {
|
|
83
|
+
try {
|
|
84
|
+
const json = Buffer.from(token, 'base64').toString('utf8');
|
|
85
|
+
return JSON.parse(json);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = Helper;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const Product = require('./Resources/Product');
|
|
2
|
+
const Order = require('./Resources/Order');
|
|
3
|
+
const Customer = require('./Resources/Customer');
|
|
4
|
+
const Webhook = require('./Resources/Webhook');
|
|
5
|
+
const Site = require('./Resources/Site');
|
|
6
|
+
const Theme = require('./Resources/Theme');
|
|
7
|
+
|
|
8
|
+
class JoonWebAPI {
|
|
9
|
+
constructor(accessToken = null, siteDomain = null) {
|
|
10
|
+
this.accessToken = accessToken;
|
|
11
|
+
this.siteDomain = siteDomain;
|
|
12
|
+
|
|
13
|
+
// Initialize resources
|
|
14
|
+
this.product = new Product(accessToken, siteDomain);
|
|
15
|
+
this.order = new Order(accessToken, siteDomain);
|
|
16
|
+
this.customer = new Customer(accessToken, siteDomain);
|
|
17
|
+
this.webhook = new Webhook(accessToken, siteDomain);
|
|
18
|
+
this.site = new Site(accessToken, siteDomain);
|
|
19
|
+
this.theme = new Theme(accessToken, siteDomain);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setAccessToken(token) {
|
|
23
|
+
this.accessToken = token;
|
|
24
|
+
|
|
25
|
+
// Update all resources
|
|
26
|
+
Object.values(this).forEach(resource => {
|
|
27
|
+
if (resource && typeof resource.setAccessToken === 'function') {
|
|
28
|
+
resource.setAccessToken(token);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setSiteDomain(domain) {
|
|
36
|
+
this.siteDomain = domain;
|
|
37
|
+
|
|
38
|
+
// Update all resources
|
|
39
|
+
Object.values(this).forEach(resource => {
|
|
40
|
+
if (resource && typeof resource.setSiteDomain === 'function') {
|
|
41
|
+
resource.setSiteDomain(domain);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Exchange authorization code for access token
|
|
50
|
+
*/
|
|
51
|
+
async exchangeCodeForToken(code, siteDomain) {
|
|
52
|
+
const apiVersion = process.env.JOONWEB_API_VERSION || '26.0';
|
|
53
|
+
const url = `https://${siteDomain}/api/admin/${apiVersion}/oauth/access_token`;
|
|
54
|
+
|
|
55
|
+
// Use axios (already in dependencies)
|
|
56
|
+
const axios = require('axios');
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const response = await axios.post(url, {
|
|
60
|
+
client_id: process.env.JOONWEB_CLIENT_ID,
|
|
61
|
+
client_secret: process.env.JOONWEB_CLIENT_SECRET,
|
|
62
|
+
code: code
|
|
63
|
+
}, {
|
|
64
|
+
headers: {
|
|
65
|
+
'Content-Type': 'application/json',
|
|
66
|
+
'Accept': 'application/json'
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return response.data;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error.response) {
|
|
73
|
+
throw new Error(`Token exchange failed: HTTP ${error.response.status} - ${JSON.stringify(error.response.data)}`);
|
|
74
|
+
} else {
|
|
75
|
+
throw new Error(`Token exchange failed: ${error.message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = JoonWebAPI;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class BaseResource {
|
|
2
|
+
constructor(httpClient, resourcePath) {
|
|
3
|
+
this.httpClient = httpClient;
|
|
4
|
+
this.resourcePath = resourcePath;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
async all(params = {}) {
|
|
8
|
+
return this.httpClient.get(this.resourcePath, params);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async get(id, params = {}) {
|
|
12
|
+
return this.httpClient.get(`${this.resourcePath}/${id}`, params);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async create(data) {
|
|
16
|
+
return this.httpClient.post(this.resourcePath, data);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async update(id, data) {
|
|
20
|
+
return this.httpClient.put(`${this.resourcePath}/${id}`, data);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async delete(id) {
|
|
24
|
+
return this.httpClient.delete(`${this.resourcePath}/${id}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async count(params = {}) {
|
|
28
|
+
return this.httpClient.get(`${this.resourcePath}/count`, params);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = BaseResource;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const BaseClient = require('../Clients/BaseClient');
|
|
2
|
+
|
|
3
|
+
class Customer extends BaseClient {
|
|
4
|
+
|
|
5
|
+
async all(params = {}) {
|
|
6
|
+
return this.request('/customers.json', 'GET', null, params);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async get(id) {
|
|
10
|
+
return this.request(`/customers/${id}.json`, 'GET');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async create(customerData) {
|
|
14
|
+
return this.request('/customers.json', 'POST', { customer: customerData });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async update(id, customerData) {
|
|
18
|
+
return this.request(`/customers/${id}.json`, 'PUT', { customer: customerData });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async delete(id) {
|
|
22
|
+
return this.request(`/customers/${id}.json`, 'DELETE');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async orders(id, params = {}) {
|
|
26
|
+
return this.request(`/customers/${id}/orders.json`, 'GET', null, params);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async addresses(id, params = {}) {
|
|
30
|
+
return this.request(`/customers/${id}/addresses.json`, 'GET', null, params);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = Customer;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const BaseClient = require('../Clients/BaseClient');
|
|
2
|
+
|
|
3
|
+
class Order extends BaseClient {
|
|
4
|
+
|
|
5
|
+
async all(params = {}) {
|
|
6
|
+
return this.request('/orders.json', 'GET', null, params);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async get(id) {
|
|
10
|
+
return this.request(`/orders/${id}.json`, 'GET');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async count(params = {}) {
|
|
14
|
+
return this.request('/orders/count.json', 'GET', null, params);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = Order;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const BaseClient = require('../Clients/BaseClient');
|
|
2
|
+
|
|
3
|
+
class Product extends BaseClient {
|
|
4
|
+
|
|
5
|
+
async all(params = {}) {
|
|
6
|
+
return this.request('/products.json', 'GET', null, params);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async get(id) {
|
|
10
|
+
return this.request(`/products/${id}.json`, 'GET');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async create(productData) {
|
|
14
|
+
return this.request('/products.json', 'POST', { product: productData });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async update(id, productData) {
|
|
18
|
+
return this.request(`/products/${id}.json`, 'PUT', { product: productData });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async delete(id) {
|
|
22
|
+
return this.request(`/products/${id}.json`, 'DELETE');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async count(params = {}) {
|
|
26
|
+
return this.request('/products/count.json', 'GET', null, params);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = Product;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const BaseClient = require('../Clients/BaseClient');
|
|
2
|
+
|
|
3
|
+
class Webhook extends BaseClient {
|
|
4
|
+
|
|
5
|
+
async all() {
|
|
6
|
+
return this.get('/webhooks.json');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async create(webhookData) {
|
|
10
|
+
return this.post('/webhooks.json', { webhook: webhookData });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async delete(id) {
|
|
14
|
+
return this.delete(`/webhooks/${id}.json`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async get(id) {
|
|
18
|
+
return this.get(`/webhooks/${id}.json`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async update(id, webhookData) {
|
|
22
|
+
return this.put(`/webhooks/${id}.json`, { webhook: webhookData });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async count() {
|
|
26
|
+
return this.get('/webhooks/count.json');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = Webhook;
|