@zero-server/middleware 0.9.1 → 0.9.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/LICENSE +21 -21
- package/index.js +13 -13
- package/lib/debug.js +372 -0
- package/lib/middleware/compress.js +230 -0
- package/lib/middleware/cookieParser.js +237 -0
- package/lib/middleware/cors.js +93 -0
- package/lib/middleware/csrf.js +137 -0
- package/lib/middleware/errorHandler.js +101 -0
- package/lib/middleware/helmet.js +176 -0
- package/lib/middleware/index.js +19 -0
- package/lib/middleware/logger.js +74 -0
- package/lib/middleware/rateLimit.js +88 -0
- package/lib/middleware/requestId.js +54 -0
- package/lib/middleware/static.js +326 -0
- package/lib/middleware/timeout.js +72 -0
- package/lib/middleware/validator.js +255 -0
- package/package.json +11 -2
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module helmet
|
|
3
|
+
* @description Security headers middleware.
|
|
4
|
+
* Sets common security-related HTTP response headers to help
|
|
5
|
+
* protect against well-known web vulnerabilities (XSS, clickjacking,
|
|
6
|
+
* MIME sniffing, etc.).
|
|
7
|
+
*
|
|
8
|
+
* Inspired by the `helmet` npm package but zero-dependency.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a security headers middleware.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} [opts] - Configuration options.
|
|
15
|
+
* @param {object|false} [opts.contentSecurityPolicy] - CSP directive object or `false` to disable.
|
|
16
|
+
* @param {boolean} [opts.crossOriginEmbedderPolicy=false] - Set COEP header.
|
|
17
|
+
* @param {string|false} [opts.crossOriginOpenerPolicy='same-origin'] - COOP value.
|
|
18
|
+
* @param {string|false} [opts.crossOriginResourcePolicy='same-origin'] - CORP value.
|
|
19
|
+
* @param {boolean} [opts.dnsPrefetchControl=true] - Set X-DNS-Prefetch-Control: off.
|
|
20
|
+
* @param {string|false} [opts.frameguard='deny'] - X-Frame-Options value ('deny' | 'sameorigin').
|
|
21
|
+
* @param {boolean} [opts.hidePoweredBy=true] - Remove X-Powered-By header.
|
|
22
|
+
* @param {boolean|number}[opts.hsts=true] - Set Strict-Transport-Security.
|
|
23
|
+
* @param {number} [opts.hstsMaxAge=15552000] - HSTS max-age in seconds (default ~180 days).
|
|
24
|
+
* @param {boolean} [opts.hstsIncludeSubDomains=true] - HSTS includeSubDomains directive.
|
|
25
|
+
* @param {boolean} [opts.hstsPreload=false] - HSTS preload directive.
|
|
26
|
+
* @param {boolean} [opts.ieNoOpen=true] - Set X-Download-Options: noopen.
|
|
27
|
+
* @param {boolean} [opts.noSniff=true] - Set X-Content-Type-Options: nosniff.
|
|
28
|
+
* @param {string|false} [opts.permittedCrossDomainPolicies='none'] - X-Permitted-Cross-Domain-Policies.
|
|
29
|
+
* @param {string|false} [opts.referrerPolicy='no-referrer'] - Referrer-Policy value.
|
|
30
|
+
* @param {boolean} [opts.xssFilter=false] - Set X-XSS-Protection (legacy, off by default).
|
|
31
|
+
* @returns {Function} Middleware `(req, res, next) => void`.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* app.use(helmet());
|
|
35
|
+
* app.use(helmet({ frameguard: 'sameorigin', hsts: false }));
|
|
36
|
+
* app.use(helmet({
|
|
37
|
+
* contentSecurityPolicy: {
|
|
38
|
+
* directives: {
|
|
39
|
+
* defaultSrc: ["'self'"],
|
|
40
|
+
* scriptSrc: ["'self'", "'unsafe-inline'"],
|
|
41
|
+
* styleSrc: ["'self'", "'unsafe-inline'"],
|
|
42
|
+
* imgSrc: ["'self'", "data:", "https:"],
|
|
43
|
+
* }
|
|
44
|
+
* }
|
|
45
|
+
* }));
|
|
46
|
+
*/
|
|
47
|
+
function helmet(opts = {})
|
|
48
|
+
{
|
|
49
|
+
return (req, res, next) =>
|
|
50
|
+
{
|
|
51
|
+
const raw = res.raw || res;
|
|
52
|
+
|
|
53
|
+
// -- Content-Security-Policy --------------------
|
|
54
|
+
if (opts.contentSecurityPolicy !== false)
|
|
55
|
+
{
|
|
56
|
+
const csp = opts.contentSecurityPolicy || {};
|
|
57
|
+
const directives = csp.directives || {
|
|
58
|
+
defaultSrc: ["'self'"],
|
|
59
|
+
baseUri: ["'self'"],
|
|
60
|
+
fontSrc: ["'self'", 'https:', 'data:'],
|
|
61
|
+
formAction: ["'self'"],
|
|
62
|
+
frameAncestors: ["'self'"],
|
|
63
|
+
imgSrc: ["'self'", 'data:'],
|
|
64
|
+
objectSrc: ["'none'"],
|
|
65
|
+
scriptSrc: ["'self'"],
|
|
66
|
+
scriptSrcAttr: ["'none'"],
|
|
67
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
68
|
+
upgradeInsecureRequests: [],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const cspString = Object.entries(directives)
|
|
72
|
+
.map(([key, values]) =>
|
|
73
|
+
{
|
|
74
|
+
const directive = key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
75
|
+
if (Array.isArray(values) && values.length === 0) return directive;
|
|
76
|
+
return `${directive} ${Array.isArray(values) ? values.join(' ') : values}`;
|
|
77
|
+
})
|
|
78
|
+
.join('; ');
|
|
79
|
+
|
|
80
|
+
if (cspString)
|
|
81
|
+
{
|
|
82
|
+
try { raw.setHeader('Content-Security-Policy', cspString); } catch (e) { }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// -- Cross-Origin-Embedder-Policy ---------------
|
|
87
|
+
if (opts.crossOriginEmbedderPolicy)
|
|
88
|
+
{
|
|
89
|
+
try { raw.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); } catch (e) { }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// -- Cross-Origin-Opener-Policy -----------------
|
|
93
|
+
if (opts.crossOriginOpenerPolicy !== false)
|
|
94
|
+
{
|
|
95
|
+
const coop = opts.crossOriginOpenerPolicy || 'same-origin';
|
|
96
|
+
try { raw.setHeader('Cross-Origin-Opener-Policy', coop); } catch (e) { }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// -- Cross-Origin-Resource-Policy ---------------
|
|
100
|
+
if (opts.crossOriginResourcePolicy !== false)
|
|
101
|
+
{
|
|
102
|
+
const corp = opts.crossOriginResourcePolicy || 'same-origin';
|
|
103
|
+
try { raw.setHeader('Cross-Origin-Resource-Policy', corp); } catch (e) { }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// -- DNS Prefetch Control -----------------------
|
|
107
|
+
if (opts.dnsPrefetchControl !== false)
|
|
108
|
+
{
|
|
109
|
+
try { raw.setHeader('X-DNS-Prefetch-Control', 'off'); } catch (e) { }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// -- Frameguard (X-Frame-Options) ---------------
|
|
113
|
+
if (opts.frameguard !== false)
|
|
114
|
+
{
|
|
115
|
+
const frame = (opts.frameguard || 'deny').toUpperCase();
|
|
116
|
+
try { raw.setHeader('X-Frame-Options', frame); } catch (e) { }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// -- Hide X-Powered-By -------------------------
|
|
120
|
+
if (opts.hidePoweredBy !== false)
|
|
121
|
+
{
|
|
122
|
+
try { raw.removeHeader('X-Powered-By'); } catch (e) { }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// -- HSTS ---------------------------------------
|
|
126
|
+
if (opts.hsts !== false)
|
|
127
|
+
{
|
|
128
|
+
const maxAge = opts.hstsMaxAge || 15552000;
|
|
129
|
+
let hstsValue = `max-age=${maxAge}`;
|
|
130
|
+
if (opts.hstsIncludeSubDomains !== false) hstsValue += '; includeSubDomains';
|
|
131
|
+
if (opts.hstsPreload) hstsValue += '; preload';
|
|
132
|
+
try { raw.setHeader('Strict-Transport-Security', hstsValue); } catch (e) { }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// -- IE No Open --------------------------------
|
|
136
|
+
if (opts.ieNoOpen !== false)
|
|
137
|
+
{
|
|
138
|
+
try { raw.setHeader('X-Download-Options', 'noopen'); } catch (e) { }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// -- No Sniff -----------------------------------
|
|
142
|
+
if (opts.noSniff !== false)
|
|
143
|
+
{
|
|
144
|
+
try { raw.setHeader('X-Content-Type-Options', 'nosniff'); } catch (e) { }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// -- Permitted Cross Domain Policies ------------
|
|
148
|
+
if (opts.permittedCrossDomainPolicies !== false)
|
|
149
|
+
{
|
|
150
|
+
const pcdp = opts.permittedCrossDomainPolicies || 'none';
|
|
151
|
+
try { raw.setHeader('X-Permitted-Cross-Domain-Policies', pcdp); } catch (e) { }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// -- Referrer Policy ----------------------------
|
|
155
|
+
if (opts.referrerPolicy !== false)
|
|
156
|
+
{
|
|
157
|
+
const rp = opts.referrerPolicy || 'no-referrer';
|
|
158
|
+
try { raw.setHeader('Referrer-Policy', rp); } catch (e) { }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// -- XSS Filter (legacy) -----------------------
|
|
162
|
+
if (opts.xssFilter)
|
|
163
|
+
{
|
|
164
|
+
try { raw.setHeader('X-XSS-Protection', '1; mode=block'); } catch (e) { }
|
|
165
|
+
}
|
|
166
|
+
else
|
|
167
|
+
{
|
|
168
|
+
// Modern best practice: disable legacy XSS auditor
|
|
169
|
+
try { raw.setHeader('X-XSS-Protection', '0'); } catch (e) { }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
next();
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = helmet;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module middleware
|
|
3
|
+
* @description Built-in middleware for zero-server.
|
|
4
|
+
* Re-exports all middleware.
|
|
5
|
+
*/
|
|
6
|
+
const cors = require('./cors');
|
|
7
|
+
const logger = require('./logger');
|
|
8
|
+
const rateLimit = require('./rateLimit');
|
|
9
|
+
const compress = require('./compress');
|
|
10
|
+
const serveStatic = require('./static');
|
|
11
|
+
const helmet = require('./helmet');
|
|
12
|
+
const timeout = require('./timeout');
|
|
13
|
+
const requestId = require('./requestId');
|
|
14
|
+
const cookieParser = require('./cookieParser');
|
|
15
|
+
const errorHandler = require('./errorHandler');
|
|
16
|
+
const csrf = require('./csrf');
|
|
17
|
+
const validate = require('./validator');
|
|
18
|
+
|
|
19
|
+
module.exports = { cors, logger, rateLimit, compress, static: serveStatic, helmet, timeout, requestId, cookieParser, errorHandler, csrf, validate };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module middleware/logger
|
|
3
|
+
* @description Simple request-logging middleware.
|
|
4
|
+
* Logs method, url, status code, and response time.
|
|
5
|
+
*
|
|
6
|
+
* @param {object} [opts] - Configuration options.
|
|
7
|
+
* @param {function} [opts.logger] - Custom log function (default: console.log).
|
|
8
|
+
* @param {boolean} [opts.colors] - Colorize output (default: true when TTY).
|
|
9
|
+
* @param {string} [opts.format] - 'tiny' | 'short' | 'dev' (default: 'dev').
|
|
10
|
+
* @returns {Function} Middleware `(req, res, next) => void`.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* app.use(logger()); // default 'dev' format
|
|
14
|
+
* app.use(logger({ format: 'tiny' })); // minimal output
|
|
15
|
+
* app.use(logger({ colors: false, logger: msg => fs.appendFileSync('access.log', msg + '\n') }));
|
|
16
|
+
*/
|
|
17
|
+
function logger(opts = {})
|
|
18
|
+
{
|
|
19
|
+
const log = typeof opts.logger === 'function' ? opts.logger : console.log;
|
|
20
|
+
const useColors = opts.colors !== undefined ? opts.colors : (process.stdout.isTTY || false);
|
|
21
|
+
const format = opts.format || 'dev';
|
|
22
|
+
|
|
23
|
+
// ANSI color helpers
|
|
24
|
+
const c = {
|
|
25
|
+
reset: useColors ? '\x1b[0m' : '',
|
|
26
|
+
green: useColors ? '\x1b[32m' : '',
|
|
27
|
+
yellow: useColors ? '\x1b[33m' : '',
|
|
28
|
+
red: useColors ? '\x1b[31m' : '',
|
|
29
|
+
cyan: useColors ? '\x1b[36m' : '',
|
|
30
|
+
dim: useColors ? '\x1b[2m' : '',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function statusColor(code)
|
|
34
|
+
{
|
|
35
|
+
if (code >= 500) return c.red;
|
|
36
|
+
if (code >= 400) return c.yellow;
|
|
37
|
+
if (code >= 300) return c.cyan;
|
|
38
|
+
return c.green;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (req, res, next) =>
|
|
42
|
+
{
|
|
43
|
+
const start = Date.now();
|
|
44
|
+
|
|
45
|
+
// Hook into the raw response 'finish' event
|
|
46
|
+
const raw = res.raw;
|
|
47
|
+
const onFinish = () =>
|
|
48
|
+
{
|
|
49
|
+
raw.removeListener('finish', onFinish);
|
|
50
|
+
const ms = Date.now() - start;
|
|
51
|
+
const status = raw.statusCode || res._status;
|
|
52
|
+
const sc = statusColor(status);
|
|
53
|
+
|
|
54
|
+
if (format === 'tiny')
|
|
55
|
+
{
|
|
56
|
+
log(`${req.method} ${req.url} ${status} - ${ms}ms`);
|
|
57
|
+
}
|
|
58
|
+
else if (format === 'short')
|
|
59
|
+
{
|
|
60
|
+
log(`${req.ip || '-'} ${req.method} ${req.url} ${sc}${status}${c.reset} ${ms}ms`);
|
|
61
|
+
}
|
|
62
|
+
else
|
|
63
|
+
{
|
|
64
|
+
// dev format
|
|
65
|
+
log(` ${c.dim}${req.method}${c.reset} ${req.url} ${sc}${status}${c.reset} ${c.dim}${ms}ms${c.reset}`);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
raw.on('finish', onFinish);
|
|
69
|
+
|
|
70
|
+
next();
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = logger;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module middleware/rateLimit
|
|
3
|
+
* @description In-memory rate-limiting middleware.
|
|
4
|
+
* Limits requests per IP address within a fixed time window.
|
|
5
|
+
*/
|
|
6
|
+
const log = require('../debug')('zero:rateLimit');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a rate-limiting middleware.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} [opts] - Configuration options.
|
|
12
|
+
* @param {number} [opts.windowMs=60000] - Time window in milliseconds.
|
|
13
|
+
* @param {number} [opts.max=100] - Maximum requests per window per IP.
|
|
14
|
+
* @param {string} [opts.message] - Custom error message.
|
|
15
|
+
* @param {number} [opts.statusCode=429] - HTTP status for rate-limited responses.
|
|
16
|
+
* @param {function} [opts.keyGenerator] - (req) => string; custom key extraction (default: req.ip).
|
|
17
|
+
* @param {function} [opts.skip] - (req) => boolean; return true to skip rate limiting.
|
|
18
|
+
* @param {function} [opts.handler] - (req, res) => void; custom handler for rate-limited requests.
|
|
19
|
+
* @returns {Function} Middleware `(req, res, next) => void`.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* app.use(rateLimit()); // 100 req/min per IP
|
|
23
|
+
* app.use(rateLimit({ windowMs: 15 * 60000, max: 50 })); // 50 req per 15 min
|
|
24
|
+
* app.use(rateLimit({
|
|
25
|
+
* max: 10,
|
|
26
|
+
* keyGenerator: req => req.headers['x-api-key'],
|
|
27
|
+
* skip: req => req.path === '/health',
|
|
28
|
+
* }));
|
|
29
|
+
*/
|
|
30
|
+
function rateLimit(opts = {})
|
|
31
|
+
{
|
|
32
|
+
const windowMs = opts.windowMs || 60_000;
|
|
33
|
+
const max = opts.max || 100;
|
|
34
|
+
const statusCode = opts.statusCode || 429;
|
|
35
|
+
const message = opts.message || 'Too many requests, please try again later.';
|
|
36
|
+
const keyGenerator = typeof opts.keyGenerator === 'function' ? opts.keyGenerator : (req) => req.ip || 'unknown';
|
|
37
|
+
const skipFn = typeof opts.skip === 'function' ? opts.skip : null;
|
|
38
|
+
const handlerFn = typeof opts.handler === 'function' ? opts.handler : null;
|
|
39
|
+
|
|
40
|
+
const hits = new Map(); // key → { count, resetTime }
|
|
41
|
+
|
|
42
|
+
// Periodic cleanup to prevent memory leaks
|
|
43
|
+
const cleanupInterval = setInterval(() =>
|
|
44
|
+
{
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
for (const [key, entry] of hits)
|
|
47
|
+
{
|
|
48
|
+
if (now >= entry.resetTime) hits.delete(key);
|
|
49
|
+
}
|
|
50
|
+
}, windowMs);
|
|
51
|
+
if (cleanupInterval.unref) cleanupInterval.unref();
|
|
52
|
+
|
|
53
|
+
return (req, res, next) =>
|
|
54
|
+
{
|
|
55
|
+
// Allow skipping rate limit for certain requests
|
|
56
|
+
if (skipFn && skipFn(req)) return next();
|
|
57
|
+
|
|
58
|
+
const key = keyGenerator(req);
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
let entry = hits.get(key);
|
|
61
|
+
|
|
62
|
+
if (!entry || now >= entry.resetTime)
|
|
63
|
+
{
|
|
64
|
+
entry = { count: 0, resetTime: now + windowMs };
|
|
65
|
+
hits.set(key, entry);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
entry.count++;
|
|
69
|
+
|
|
70
|
+
// Set rate-limit headers
|
|
71
|
+
const remaining = Math.max(0, max - entry.count);
|
|
72
|
+
res.set('X-RateLimit-Limit', String(max));
|
|
73
|
+
res.set('X-RateLimit-Remaining', String(remaining));
|
|
74
|
+
res.set('X-RateLimit-Reset', String(Math.ceil(entry.resetTime / 1000)));
|
|
75
|
+
|
|
76
|
+
if (entry.count > max)
|
|
77
|
+
{
|
|
78
|
+
log.warn('rate limit exceeded for %s', key);
|
|
79
|
+
res.set('Retry-After', String(Math.ceil(windowMs / 1000)));
|
|
80
|
+
if (handlerFn) return handlerFn(req, res);
|
|
81
|
+
return res.status(statusCode).json({ error: message });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
next();
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = rateLimit;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module requestId
|
|
3
|
+
* @description Request ID middleware.
|
|
4
|
+
* Assigns a unique identifier to each incoming request for
|
|
5
|
+
* tracing and debugging. Sets the ID on both the request
|
|
6
|
+
* object and as a response header.
|
|
7
|
+
*/
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a request ID middleware.
|
|
12
|
+
*
|
|
13
|
+
* @param {object} [opts] - Configuration options.
|
|
14
|
+
* @param {string} [opts.header='X-Request-Id'] - Response header name.
|
|
15
|
+
* @param {Function} [opts.generator] - Custom ID generator `() => string`.
|
|
16
|
+
* @param {boolean} [opts.trustProxy=false] - Trust incoming X-Request-Id header from proxy.
|
|
17
|
+
* @returns {Function} Middleware `(req, res, next) => void`.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* app.use(requestId());
|
|
21
|
+
* app.get('/', (req, res) => {
|
|
22
|
+
* console.log(req.id); // e.g. '7f3a2b1c-...'
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
function requestId(opts = {})
|
|
26
|
+
{
|
|
27
|
+
const headerName = opts.header || 'X-Request-Id';
|
|
28
|
+
const trustProxy = !!opts.trustProxy;
|
|
29
|
+
const generator = typeof opts.generator === 'function'
|
|
30
|
+
? opts.generator
|
|
31
|
+
: () => crypto.randomUUID();
|
|
32
|
+
|
|
33
|
+
return (req, res, next) =>
|
|
34
|
+
{
|
|
35
|
+
let id;
|
|
36
|
+
|
|
37
|
+
if (trustProxy)
|
|
38
|
+
{
|
|
39
|
+
const existing = req.headers[headerName.toLowerCase()];
|
|
40
|
+
if (existing && typeof existing === 'string' && existing.length <= 128)
|
|
41
|
+
{
|
|
42
|
+
id = existing;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!id) id = generator();
|
|
47
|
+
|
|
48
|
+
req.id = id;
|
|
49
|
+
res.set(headerName, id);
|
|
50
|
+
next();
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = requestId;
|