@ricardodeazambuja/browser-mcp-server 1.0.3 → 1.4.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/CHANGELOG-v1.3.0.md +42 -0
- package/CHANGELOG-v1.4.0.md +8 -0
- package/README.md +271 -45
- package/package.json +11 -10
- package/plugins/.gitkeep +0 -0
- package/src/.gitkeep +0 -0
- package/src/browser.js +152 -0
- package/src/cdp.js +58 -0
- package/src/index.js +126 -0
- package/src/tools/.gitkeep +0 -0
- package/src/tools/console.js +139 -0
- package/src/tools/docs.js +1611 -0
- package/src/tools/index.js +60 -0
- package/src/tools/info.js +139 -0
- package/src/tools/interaction.js +126 -0
- package/src/tools/keyboard.js +27 -0
- package/src/tools/media.js +264 -0
- package/src/tools/mouse.js +104 -0
- package/src/tools/navigation.js +72 -0
- package/src/tools/network.js +552 -0
- package/src/tools/pages.js +149 -0
- package/src/tools/performance.js +517 -0
- package/src/tools/security.js +470 -0
- package/src/tools/storage.js +467 -0
- package/src/tools/system.js +196 -0
- package/src/utils.js +131 -0
- package/tests/.gitkeep +0 -0
- package/tests/fixtures/.gitkeep +0 -0
- package/tests/fixtures/test-media.html +35 -0
- package/tests/fixtures/test-network.html +48 -0
- package/tests/fixtures/test-performance.html +61 -0
- package/tests/fixtures/test-security.html +33 -0
- package/tests/fixtures/test-storage.html +76 -0
- package/tests/run-all.js +50 -0
- package/{test-browser-automation.js → tests/test-browser-automation.js} +44 -5
- package/{test-mcp.js → tests/test-mcp.js} +9 -4
- package/tests/test-media-tools.js +168 -0
- package/tests/test-network.js +212 -0
- package/tests/test-performance.js +254 -0
- package/tests/test-security.js +203 -0
- package/tests/test-storage.js +192 -0
- package/CHANGELOG-v1.0.2.md +0 -126
- package/browser-mcp-server-playwright.js +0 -792
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Testing Tools (CDP-based)
|
|
3
|
+
* Security headers, TLS certificates, mixed content, CSP violations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { connectToBrowser } = require('../browser');
|
|
7
|
+
const { getCDPSession } = require('../cdp');
|
|
8
|
+
const { debugLog } = require('../utils');
|
|
9
|
+
|
|
10
|
+
// Local state for security tools
|
|
11
|
+
let cspViolations = [];
|
|
12
|
+
let cspMonitoringActive = false;
|
|
13
|
+
|
|
14
|
+
const definitions = [
|
|
15
|
+
{
|
|
16
|
+
name: 'browser_sec_get_security_headers',
|
|
17
|
+
description: 'Inspect security-related HTTP headers (see browser_docs)',
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {},
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'browser_sec_get_certificate_info',
|
|
27
|
+
description: 'Get TLS/SSL certificate details for HTTPS sites (see browser_docs)',
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {},
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'browser_sec_detect_mixed_content',
|
|
37
|
+
description: 'Detect mixed content warnings (HTTPS page loading HTTP resources) (see browser_docs)',
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {},
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'browser_sec_start_csp_monitoring',
|
|
47
|
+
description: 'Monitor Content Security Policy violations (see browser_docs)',
|
|
48
|
+
inputSchema: {
|
|
49
|
+
type: 'object',
|
|
50
|
+
properties: {},
|
|
51
|
+
additionalProperties: false,
|
|
52
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'browser_sec_get_csp_violations',
|
|
57
|
+
description: 'Get captured CSP violations (see browser_docs)',
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {},
|
|
61
|
+
additionalProperties: false,
|
|
62
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'browser_sec_stop_csp_monitoring',
|
|
67
|
+
description: 'Stop CSP monitoring and clear violations (see browser_docs)',
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {},
|
|
71
|
+
additionalProperties: false,
|
|
72
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const handlers = {
|
|
78
|
+
browser_sec_get_security_headers: async (args) => {
|
|
79
|
+
try {
|
|
80
|
+
const { page } = await connectToBrowser();
|
|
81
|
+
|
|
82
|
+
// Get headers from HTTP response or meta tags
|
|
83
|
+
const securityData = await page.evaluate(async () => {
|
|
84
|
+
const result = {
|
|
85
|
+
headers: null,
|
|
86
|
+
metaTags: {},
|
|
87
|
+
protocol: window.location.protocol
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Try to get headers using fetch (works for HTTP/HTTPS)
|
|
91
|
+
if (window.location.protocol.startsWith('http')) {
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch(window.location.href, { method: 'HEAD' });
|
|
94
|
+
const headers = {};
|
|
95
|
+
for (let [key, value] of res.headers.entries()) {
|
|
96
|
+
headers[key] = value;
|
|
97
|
+
}
|
|
98
|
+
result.headers = headers;
|
|
99
|
+
} catch (e) {
|
|
100
|
+
// Fetch failed, will fall back to meta tags
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Read security-related meta tags (works for file:// and as fallback)
|
|
105
|
+
const metaTags = document.querySelectorAll('meta[http-equiv]');
|
|
106
|
+
metaTags.forEach(tag => {
|
|
107
|
+
const httpEquiv = tag.getAttribute('http-equiv').toLowerCase();
|
|
108
|
+
const content = tag.getAttribute('content');
|
|
109
|
+
if (httpEquiv && content) {
|
|
110
|
+
result.metaTags[httpEquiv] = content;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return result;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Build security headers object from HTTP headers or meta tags
|
|
118
|
+
const securityHeaders = {
|
|
119
|
+
'content-security-policy':
|
|
120
|
+
securityData.headers?.['content-security-policy'] ||
|
|
121
|
+
securityData.metaTags['content-security-policy'] ||
|
|
122
|
+
'Not set',
|
|
123
|
+
'strict-transport-security':
|
|
124
|
+
securityData.headers?.['strict-transport-security'] ||
|
|
125
|
+
'Not set',
|
|
126
|
+
'x-frame-options':
|
|
127
|
+
securityData.headers?.['x-frame-options'] ||
|
|
128
|
+
securityData.metaTags['x-frame-options'] ||
|
|
129
|
+
'Not set',
|
|
130
|
+
'x-content-type-options':
|
|
131
|
+
securityData.headers?.['x-content-type-options'] ||
|
|
132
|
+
'Not set',
|
|
133
|
+
'referrer-policy':
|
|
134
|
+
securityData.headers?.['referrer-policy'] ||
|
|
135
|
+
securityData.metaTags['referrer-policy'] ||
|
|
136
|
+
'Not set',
|
|
137
|
+
'permissions-policy':
|
|
138
|
+
securityData.headers?.['permissions-policy'] ||
|
|
139
|
+
'Not set',
|
|
140
|
+
'x-xss-protection':
|
|
141
|
+
securityData.headers?.['x-xss-protection'] ||
|
|
142
|
+
'Not set (deprecated)'
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const source = securityData.headers ? 'HTTP headers' : 'meta tags';
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
content: [{
|
|
149
|
+
type: 'text',
|
|
150
|
+
text: `🔒 Security Headers (from ${source}):\n\n${JSON.stringify(securityHeaders, null, 2)}`
|
|
151
|
+
}]
|
|
152
|
+
};
|
|
153
|
+
} catch (error) {
|
|
154
|
+
debugLog(`Error in browser_sec_get_security_headers: ${error.message}`);
|
|
155
|
+
return {
|
|
156
|
+
content: [{
|
|
157
|
+
type: 'text',
|
|
158
|
+
text: `❌ Error: ${error.message}`
|
|
159
|
+
}],
|
|
160
|
+
isError: true
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
browser_sec_get_certificate_info: async (args) => {
|
|
166
|
+
try {
|
|
167
|
+
const { page } = await connectToBrowser();
|
|
168
|
+
const url = page.url();
|
|
169
|
+
|
|
170
|
+
if (!url.startsWith('https://')) {
|
|
171
|
+
return {
|
|
172
|
+
content: [{
|
|
173
|
+
type: 'text',
|
|
174
|
+
text: '⚠️ Certificate information only available for HTTPS sites.\n\nCurrent page is not using HTTPS.'
|
|
175
|
+
}]
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const cdp = await getCDPSession();
|
|
180
|
+
await cdp.send('Security.enable');
|
|
181
|
+
|
|
182
|
+
// Get security state which includes certificate info
|
|
183
|
+
const securityState = await page.evaluate(async () => {
|
|
184
|
+
// Try to get security info from the page context
|
|
185
|
+
return {
|
|
186
|
+
url: window.location.href,
|
|
187
|
+
protocol: window.location.protocol
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Note: Getting detailed certificate info via CDP is complex
|
|
192
|
+
// as it requires monitoring security state changes during navigation
|
|
193
|
+
// For now, provide basic HTTPS validation info
|
|
194
|
+
|
|
195
|
+
const certInfo = {
|
|
196
|
+
url: url,
|
|
197
|
+
protocol: 'HTTPS',
|
|
198
|
+
secure: true,
|
|
199
|
+
note: 'Detailed certificate inspection requires monitoring during page load. For full certificate details, use browser DevTools Security panel.'
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
await cdp.send('Security.disable');
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
content: [{
|
|
206
|
+
type: 'text',
|
|
207
|
+
text: `🔒 Certificate Information:\n\n${JSON.stringify(certInfo, null, 2)}\n\nNote: For detailed certificate information (issuer, expiry, subject), use:\n1. Chrome DevTools > Security panel\n2. Or start network monitoring before navigation to capture TLS details`
|
|
208
|
+
}]
|
|
209
|
+
};
|
|
210
|
+
} catch (error) {
|
|
211
|
+
debugLog(`CDP error in browser_sec_get_certificate_info: ${error.message}`);
|
|
212
|
+
return {
|
|
213
|
+
content: [{
|
|
214
|
+
type: 'text',
|
|
215
|
+
text: `❌ CDP Error: ${error.message}`
|
|
216
|
+
}],
|
|
217
|
+
isError: true
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
browser_sec_detect_mixed_content: async (args) => {
|
|
223
|
+
try {
|
|
224
|
+
const { page } = await connectToBrowser();
|
|
225
|
+
const url = page.url();
|
|
226
|
+
|
|
227
|
+
if (!url.startsWith('https://')) {
|
|
228
|
+
return {
|
|
229
|
+
content: [{
|
|
230
|
+
type: 'text',
|
|
231
|
+
text: '⚠️ Mixed content detection only applies to HTTPS pages.\n\nCurrent page is not using HTTPS.'
|
|
232
|
+
}]
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Detect mixed content by analyzing resources
|
|
237
|
+
const mixedContent = await page.evaluate(() => {
|
|
238
|
+
const issues = [];
|
|
239
|
+
|
|
240
|
+
// Check all loaded resources
|
|
241
|
+
performance.getEntriesByType('resource').forEach(entry => {
|
|
242
|
+
if (entry.name.startsWith('http://')) {
|
|
243
|
+
issues.push({
|
|
244
|
+
url: entry.name,
|
|
245
|
+
type: entry.initiatorType,
|
|
246
|
+
blocked: false
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Check scripts
|
|
252
|
+
document.querySelectorAll('script[src]').forEach(script => {
|
|
253
|
+
if (script.src.startsWith('http://')) {
|
|
254
|
+
issues.push({
|
|
255
|
+
url: script.src,
|
|
256
|
+
type: 'script',
|
|
257
|
+
blocked: true // Mixed scripts are usually blocked
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Check stylesheets
|
|
263
|
+
document.querySelectorAll('link[rel="stylesheet"]').forEach(link => {
|
|
264
|
+
if (link.href.startsWith('http://')) {
|
|
265
|
+
issues.push({
|
|
266
|
+
url: link.href,
|
|
267
|
+
type: 'stylesheet',
|
|
268
|
+
blocked: false
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Check images
|
|
274
|
+
document.querySelectorAll('img[src]').forEach(img => {
|
|
275
|
+
if (img.src.startsWith('http://')) {
|
|
276
|
+
issues.push({
|
|
277
|
+
url: img.src,
|
|
278
|
+
type: 'image',
|
|
279
|
+
blocked: false
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return issues;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
if (mixedContent.length === 0) {
|
|
288
|
+
return {
|
|
289
|
+
content: [{
|
|
290
|
+
type: 'text',
|
|
291
|
+
text: '✅ No mixed content detected.\n\nAll resources are loaded over HTTPS.'
|
|
292
|
+
}]
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const summary = {
|
|
297
|
+
total: mixedContent.length,
|
|
298
|
+
blocked: mixedContent.filter(i => i.blocked).length,
|
|
299
|
+
issues: mixedContent.slice(0, 20)
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
content: [{
|
|
304
|
+
type: 'text',
|
|
305
|
+
text: `⚠️ Mixed Content Detected:\n\n${JSON.stringify(summary, null, 2)}\n\nNote: Showing first 20 issues. Mixed content can be a security risk.`
|
|
306
|
+
}]
|
|
307
|
+
};
|
|
308
|
+
} catch (error) {
|
|
309
|
+
debugLog(`Error in browser_sec_detect_mixed_content: ${error.message}`);
|
|
310
|
+
return {
|
|
311
|
+
content: [{
|
|
312
|
+
type: 'text',
|
|
313
|
+
text: `❌ Error: ${error.message}`
|
|
314
|
+
}],
|
|
315
|
+
isError: true
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
browser_sec_start_csp_monitoring: async (args) => {
|
|
321
|
+
try {
|
|
322
|
+
if (cspMonitoringActive) {
|
|
323
|
+
return {
|
|
324
|
+
content: [{
|
|
325
|
+
type: 'text',
|
|
326
|
+
text: '⚠️ CSP monitoring is already active.\n\nUse browser_sec_get_csp_violations to view violations or browser_sec_stop_csp_monitoring to stop.'
|
|
327
|
+
}]
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const cdp = await getCDPSession();
|
|
332
|
+
cspViolations = [];
|
|
333
|
+
|
|
334
|
+
// Enable Log domain to capture CSP violations
|
|
335
|
+
await cdp.send('Log.enable');
|
|
336
|
+
|
|
337
|
+
// Listen for log entries
|
|
338
|
+
cdp.on('Log.entryAdded', (params) => {
|
|
339
|
+
const entry = params.entry;
|
|
340
|
+
|
|
341
|
+
// CSP violations appear as console errors with specific text
|
|
342
|
+
if (entry.source === 'security' ||
|
|
343
|
+
(entry.text && entry.text.includes('Content Security Policy')) ||
|
|
344
|
+
(entry.text && entry.text.includes('CSP'))) {
|
|
345
|
+
|
|
346
|
+
cspViolations.push({
|
|
347
|
+
timestamp: new Date(entry.timestamp).toISOString(),
|
|
348
|
+
text: entry.text,
|
|
349
|
+
level: entry.level,
|
|
350
|
+
source: entry.source,
|
|
351
|
+
url: entry.url,
|
|
352
|
+
lineNumber: entry.lineNumber
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
cspMonitoringActive = true;
|
|
358
|
+
|
|
359
|
+
debugLog('Started CSP violation monitoring');
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
content: [{
|
|
363
|
+
type: 'text',
|
|
364
|
+
text: '✅ CSP violation monitoring started\n\nCapturing Content Security Policy violations...\n\nUse browser_sec_get_csp_violations to view captured violations.'
|
|
365
|
+
}]
|
|
366
|
+
};
|
|
367
|
+
} catch (error) {
|
|
368
|
+
debugLog(`CDP error in browser_sec_start_csp_monitoring: ${error.message}`);
|
|
369
|
+
return {
|
|
370
|
+
content: [{
|
|
371
|
+
type: 'text',
|
|
372
|
+
text: `❌ CDP Error: ${error.message}\n\nPossible causes:\n- CDP session disconnected\n- Log domain not supported`
|
|
373
|
+
}],
|
|
374
|
+
isError: true
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
browser_sec_get_csp_violations: async (args) => {
|
|
380
|
+
try {
|
|
381
|
+
if (!cspMonitoringActive) {
|
|
382
|
+
return {
|
|
383
|
+
content: [{
|
|
384
|
+
type: 'text',
|
|
385
|
+
text: '⚠️ CSP monitoring is not active.\n\nUse browser_sec_start_csp_monitoring to start monitoring first.'
|
|
386
|
+
}]
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (cspViolations.length === 0) {
|
|
391
|
+
return {
|
|
392
|
+
content: [{
|
|
393
|
+
type: 'text',
|
|
394
|
+
text: 'No CSP violations detected yet.\n\nMonitoring is active - violations will appear if any occur.'
|
|
395
|
+
}]
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const summary = {
|
|
400
|
+
total: cspViolations.length,
|
|
401
|
+
violations: cspViolations.map(v => ({
|
|
402
|
+
timestamp: v.timestamp,
|
|
403
|
+
message: v.text,
|
|
404
|
+
level: v.level,
|
|
405
|
+
source: v.url || 'unknown'
|
|
406
|
+
}))
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
content: [{
|
|
411
|
+
type: 'text',
|
|
412
|
+
text: `⚠️ CSP Violations (${cspViolations.length}):\n\n${JSON.stringify(summary, null, 2)}`
|
|
413
|
+
}]
|
|
414
|
+
};
|
|
415
|
+
} catch (error) {
|
|
416
|
+
debugLog(`Error in browser_sec_get_csp_violations: ${error.message}`);
|
|
417
|
+
return {
|
|
418
|
+
content: [{
|
|
419
|
+
type: 'text',
|
|
420
|
+
text: `❌ Error: ${error.message}`
|
|
421
|
+
}],
|
|
422
|
+
isError: true
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
|
|
427
|
+
browser_sec_stop_csp_monitoring: async (args) => {
|
|
428
|
+
try {
|
|
429
|
+
if (!cspMonitoringActive) {
|
|
430
|
+
return {
|
|
431
|
+
content: [{
|
|
432
|
+
type: 'text',
|
|
433
|
+
text: '⚠️ CSP monitoring is not active.'
|
|
434
|
+
}]
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const cdp = await getCDPSession();
|
|
439
|
+
await cdp.send('Log.disable');
|
|
440
|
+
|
|
441
|
+
// Remove listener
|
|
442
|
+
cdp.removeAllListeners('Log.entryAdded');
|
|
443
|
+
|
|
444
|
+
const count = cspViolations.length;
|
|
445
|
+
cspViolations = [];
|
|
446
|
+
cspMonitoringActive = false;
|
|
447
|
+
|
|
448
|
+
debugLog('Stopped CSP violation monitoring');
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
content: [{
|
|
452
|
+
type: 'text',
|
|
453
|
+
text: `✅ CSP monitoring stopped\n\nCaptured ${count} violations.\nData has been cleared.`
|
|
454
|
+
}]
|
|
455
|
+
};
|
|
456
|
+
} catch (error) {
|
|
457
|
+
cspMonitoringActive = false;
|
|
458
|
+
debugLog(`CDP error in browser_sec_stop_csp_monitoring: ${error.message}`);
|
|
459
|
+
return {
|
|
460
|
+
content: [{
|
|
461
|
+
type: 'text',
|
|
462
|
+
text: `❌ CDP Error: ${error.message}\n\nMonitoring has been stopped.`
|
|
463
|
+
}],
|
|
464
|
+
isError: true
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
module.exports = { definitions, handlers };
|