undici 6.7.1 → 6.8.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/docs/docs/api/Errors.md +1 -0
- package/lib/api/api-request.js +2 -1
- package/lib/api/readable.js +9 -2
- package/lib/api/util.js +53 -15
- package/lib/core/errors.js +12 -1
- package/lib/core/request.js +3 -2
- package/lib/core/util.js +11 -1
- package/lib/dispatcher/proxy-agent.js +10 -5
- package/lib/web/fetch/formdata-parser.js +29 -13
- package/lib/web/fetch/headers.js +11 -0
- package/package.json +2 -1
package/docs/docs/api/Errors.md
CHANGED
|
@@ -26,6 +26,7 @@ import { errors } from 'undici'
|
|
|
26
26
|
| `ResponseContentLengthMismatchError` | `UND_ERR_RES_CONTENT_LENGTH_MISMATCH` | response body does not match content-length header |
|
|
27
27
|
| `InformationalError` | `UND_ERR_INFO` | expected error with reason |
|
|
28
28
|
| `ResponseExceededMaxSizeError` | `UND_ERR_RES_EXCEEDED_MAX_SIZE` | response body exceed the max size allowed |
|
|
29
|
+
| `SecureProxyConnectionError` | `UND_ERR_PRX_TLS` | tls connection to a proxy failed |
|
|
29
30
|
|
|
30
31
|
### `SocketError`
|
|
31
32
|
|
package/lib/api/api-request.js
CHANGED
|
@@ -91,7 +91,8 @@ class RequestHandler extends AsyncResource {
|
|
|
91
91
|
|
|
92
92
|
const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers
|
|
93
93
|
const contentType = parsedHeaders['content-type']
|
|
94
|
-
const
|
|
94
|
+
const contentLength = parsedHeaders['content-length']
|
|
95
|
+
const body = new Readable({ resume, abort, contentType, contentLength, highWaterMark })
|
|
95
96
|
|
|
96
97
|
this.callback = null
|
|
97
98
|
this.res = body
|
package/lib/api/readable.js
CHANGED
|
@@ -11,8 +11,9 @@ const { ReadableStreamFrom } = require('../core/util')
|
|
|
11
11
|
const kConsume = Symbol('kConsume')
|
|
12
12
|
const kReading = Symbol('kReading')
|
|
13
13
|
const kBody = Symbol('kBody')
|
|
14
|
-
const kAbort = Symbol('
|
|
14
|
+
const kAbort = Symbol('kAbort')
|
|
15
15
|
const kContentType = Symbol('kContentType')
|
|
16
|
+
const kContentLength = Symbol('kContentLength')
|
|
16
17
|
|
|
17
18
|
const noop = () => {}
|
|
18
19
|
|
|
@@ -21,6 +22,7 @@ class BodyReadable extends Readable {
|
|
|
21
22
|
resume,
|
|
22
23
|
abort,
|
|
23
24
|
contentType = '',
|
|
25
|
+
contentLength,
|
|
24
26
|
highWaterMark = 64 * 1024 // Same as nodejs fs streams.
|
|
25
27
|
}) {
|
|
26
28
|
super({
|
|
@@ -35,6 +37,7 @@ class BodyReadable extends Readable {
|
|
|
35
37
|
this[kConsume] = null
|
|
36
38
|
this[kBody] = null
|
|
37
39
|
this[kContentType] = contentType
|
|
40
|
+
this[kContentLength] = contentLength
|
|
38
41
|
|
|
39
42
|
// Is stream being consumed through Readable API?
|
|
40
43
|
// This is an optimization so that we avoid checking
|
|
@@ -146,7 +149,7 @@ class BodyReadable extends Readable {
|
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
async dump (opts) {
|
|
149
|
-
let limit = Number.isFinite(opts?.limit) ? opts.limit :
|
|
152
|
+
let limit = Number.isFinite(opts?.limit) ? opts.limit : 128 * 1024
|
|
150
153
|
const signal = opts?.signal
|
|
151
154
|
|
|
152
155
|
if (signal != null && (typeof signal !== 'object' || !('aborted' in signal))) {
|
|
@@ -160,6 +163,10 @@ class BodyReadable extends Readable {
|
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
return await new Promise((resolve, reject) => {
|
|
166
|
+
if (this[kContentLength] > limit) {
|
|
167
|
+
this.destroy(new AbortError())
|
|
168
|
+
}
|
|
169
|
+
|
|
163
170
|
const onAbort = () => {
|
|
164
171
|
this.destroy(signal.reason ?? new AbortError())
|
|
165
172
|
}
|
package/lib/api/util.js
CHANGED
|
@@ -21,28 +21,66 @@ async function getResolveErrorBodyCallback ({ callback, body, contentType, statu
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
const message = `Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`
|
|
25
|
+
|
|
24
26
|
if (statusCode === 204 || !contentType || !chunks) {
|
|
25
|
-
|
|
27
|
+
queueMicrotask(() => callback(new ResponseStatusCodeError(message, statusCode, headers)))
|
|
26
28
|
return
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload))
|
|
33
|
-
return
|
|
34
|
-
}
|
|
31
|
+
const stackTraceLimit = Error.stackTraceLimit
|
|
32
|
+
Error.stackTraceLimit = 0
|
|
33
|
+
let payload
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
try {
|
|
36
|
+
if (isContentTypeApplicationJson(contentType)) {
|
|
37
|
+
payload = JSON.parse(chunksDecode(chunks, length))
|
|
38
|
+
} else if (isContentTypeText(contentType)) {
|
|
39
|
+
payload = chunksDecode(chunks, length)
|
|
40
40
|
}
|
|
41
|
-
} catch
|
|
42
|
-
//
|
|
41
|
+
} catch {
|
|
42
|
+
// process in a callback to avoid throwing in the microtask queue
|
|
43
|
+
} finally {
|
|
44
|
+
Error.stackTraceLimit = stackTraceLimit
|
|
43
45
|
}
|
|
46
|
+
queueMicrotask(() => callback(new ResponseStatusCodeError(message, statusCode, headers, payload)))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const isContentTypeApplicationJson = (contentType) => {
|
|
50
|
+
return (
|
|
51
|
+
contentType.length > 15 &&
|
|
52
|
+
contentType[11] === '/' &&
|
|
53
|
+
contentType[0] === 'a' &&
|
|
54
|
+
contentType[1] === 'p' &&
|
|
55
|
+
contentType[2] === 'p' &&
|
|
56
|
+
contentType[3] === 'l' &&
|
|
57
|
+
contentType[4] === 'i' &&
|
|
58
|
+
contentType[5] === 'c' &&
|
|
59
|
+
contentType[6] === 'a' &&
|
|
60
|
+
contentType[7] === 't' &&
|
|
61
|
+
contentType[8] === 'i' &&
|
|
62
|
+
contentType[9] === 'o' &&
|
|
63
|
+
contentType[10] === 'n' &&
|
|
64
|
+
contentType[12] === 'j' &&
|
|
65
|
+
contentType[13] === 's' &&
|
|
66
|
+
contentType[14] === 'o' &&
|
|
67
|
+
contentType[15] === 'n'
|
|
68
|
+
)
|
|
69
|
+
}
|
|
44
70
|
|
|
45
|
-
|
|
71
|
+
const isContentTypeText = (contentType) => {
|
|
72
|
+
return (
|
|
73
|
+
contentType.length > 4 &&
|
|
74
|
+
contentType[4] === '/' &&
|
|
75
|
+
contentType[0] === 't' &&
|
|
76
|
+
contentType[1] === 'e' &&
|
|
77
|
+
contentType[2] === 'x' &&
|
|
78
|
+
contentType[3] === 't'
|
|
79
|
+
)
|
|
46
80
|
}
|
|
47
81
|
|
|
48
|
-
module.exports = {
|
|
82
|
+
module.exports = {
|
|
83
|
+
getResolveErrorBodyCallback,
|
|
84
|
+
isContentTypeApplicationJson,
|
|
85
|
+
isContentTypeText
|
|
86
|
+
}
|
package/lib/core/errors.js
CHANGED
|
@@ -195,6 +195,16 @@ class RequestRetryError extends UndiciError {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
class SecureProxyConnectionError extends UndiciError {
|
|
199
|
+
constructor (cause, message, options) {
|
|
200
|
+
super(message, { cause, ...(options ?? {}) })
|
|
201
|
+
this.name = 'SecureProxyConnectionError'
|
|
202
|
+
this.message = message || 'Secure Proxy Connection failed'
|
|
203
|
+
this.code = 'UND_ERR_PRX_TLS'
|
|
204
|
+
this.cause = cause
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
198
208
|
module.exports = {
|
|
199
209
|
AbortError,
|
|
200
210
|
HTTPParserError,
|
|
@@ -216,5 +226,6 @@ module.exports = {
|
|
|
216
226
|
ResponseContentLengthMismatchError,
|
|
217
227
|
BalancedPoolMissingUpstreamError,
|
|
218
228
|
ResponseExceededMaxSizeError,
|
|
219
|
-
RequestRetryError
|
|
229
|
+
RequestRetryError,
|
|
230
|
+
SecureProxyConnectionError
|
|
220
231
|
}
|
package/lib/core/request.js
CHANGED
|
@@ -40,7 +40,8 @@ class Request {
|
|
|
40
40
|
bodyTimeout,
|
|
41
41
|
reset,
|
|
42
42
|
throwOnError,
|
|
43
|
-
expectContinue
|
|
43
|
+
expectContinue,
|
|
44
|
+
servername
|
|
44
45
|
}, handler) {
|
|
45
46
|
if (typeof path !== 'string') {
|
|
46
47
|
throw new InvalidArgumentError('path must be a string')
|
|
@@ -181,7 +182,7 @@ class Request {
|
|
|
181
182
|
|
|
182
183
|
validateHandler(handler, method, upgrade)
|
|
183
184
|
|
|
184
|
-
this.servername = getServerName(this.host)
|
|
185
|
+
this.servername = servername || getServerName(this.host)
|
|
185
186
|
|
|
186
187
|
this[kHandler] = handler
|
|
187
188
|
|
package/lib/core/util.js
CHANGED
|
@@ -440,7 +440,8 @@ function addAbortListener (signal, listener) {
|
|
|
440
440
|
return () => signal.removeListener('abort', listener)
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
-
const hasToWellFormed =
|
|
443
|
+
const hasToWellFormed = typeof String.prototype.toWellFormed === 'function'
|
|
444
|
+
const hasIsWellFormed = typeof String.prototype.isWellFormed === 'function'
|
|
444
445
|
|
|
445
446
|
/**
|
|
446
447
|
* @param {string} val
|
|
@@ -449,6 +450,14 @@ function toUSVString (val) {
|
|
|
449
450
|
return hasToWellFormed ? `${val}`.toWellFormed() : nodeUtil.toUSVString(val)
|
|
450
451
|
}
|
|
451
452
|
|
|
453
|
+
/**
|
|
454
|
+
* @param {string} val
|
|
455
|
+
*/
|
|
456
|
+
// TODO: move this to webidl
|
|
457
|
+
function isUSVString (val) {
|
|
458
|
+
return hasIsWellFormed ? `${val}`.isWellFormed() : toUSVString(val) === `${val}`
|
|
459
|
+
}
|
|
460
|
+
|
|
452
461
|
/**
|
|
453
462
|
* @see https://tools.ietf.org/html/rfc7230#section-3.2.6
|
|
454
463
|
* @param {number} c
|
|
@@ -538,6 +547,7 @@ module.exports = {
|
|
|
538
547
|
isErrored,
|
|
539
548
|
isReadable,
|
|
540
549
|
toUSVString,
|
|
550
|
+
isUSVString,
|
|
541
551
|
isReadableAborted,
|
|
542
552
|
isBlobLike,
|
|
543
553
|
parseOrigin,
|
|
@@ -5,7 +5,7 @@ const { URL } = require('node:url')
|
|
|
5
5
|
const Agent = require('./agent')
|
|
6
6
|
const Pool = require('./pool')
|
|
7
7
|
const DispatcherBase = require('./dispatcher-base')
|
|
8
|
-
const { InvalidArgumentError, RequestAbortedError } = require('../core/errors')
|
|
8
|
+
const { InvalidArgumentError, RequestAbortedError, SecureProxyConnectionError } = require('../core/errors')
|
|
9
9
|
const buildConnector = require('../core/connect')
|
|
10
10
|
|
|
11
11
|
const kAgent = Symbol('proxy agent')
|
|
@@ -37,10 +37,9 @@ class ProxyAgent extends DispatcherBase {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const url = this.#getUrl(opts)
|
|
40
|
-
const { href, origin, port, protocol, username, password } = url
|
|
40
|
+
const { href, origin, port, protocol, username, password, hostname: proxyHostname } = url
|
|
41
41
|
|
|
42
42
|
this[kProxy] = { uri: href, protocol }
|
|
43
|
-
this[kAgent] = new Agent(opts)
|
|
44
43
|
this[kInterceptors] = opts.interceptors?.ProxyAgent && Array.isArray(opts.interceptors.ProxyAgent)
|
|
45
44
|
? opts.interceptors.ProxyAgent
|
|
46
45
|
: []
|
|
@@ -78,7 +77,8 @@ class ProxyAgent extends DispatcherBase {
|
|
|
78
77
|
headers: {
|
|
79
78
|
...this[kProxyHeaders],
|
|
80
79
|
host: requestedHost
|
|
81
|
-
}
|
|
80
|
+
},
|
|
81
|
+
servername: this[kProxyTls]?.servername || proxyHostname
|
|
82
82
|
})
|
|
83
83
|
if (statusCode !== 200) {
|
|
84
84
|
socket.on('error', () => {}).destroy()
|
|
@@ -96,7 +96,12 @@ class ProxyAgent extends DispatcherBase {
|
|
|
96
96
|
}
|
|
97
97
|
this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback)
|
|
98
98
|
} catch (err) {
|
|
99
|
-
|
|
99
|
+
if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
|
|
100
|
+
// Throw a custom error to avoid loop in client.js#connect
|
|
101
|
+
callback(new SecureProxyConnectionError(err))
|
|
102
|
+
} else {
|
|
103
|
+
callback(err)
|
|
104
|
+
}
|
|
100
105
|
}
|
|
101
106
|
}
|
|
102
107
|
})
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { toUSVString, isUSVString, bufferToLowerCasedHeaderName } = require('../../core/util')
|
|
4
4
|
const { utf8DecodeBytes } = require('./util')
|
|
5
5
|
const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url')
|
|
6
6
|
const { isFileLike, File: UndiciFile } = require('./file')
|
|
7
7
|
const { makeEntry } = require('./formdata')
|
|
8
8
|
const assert = require('node:assert')
|
|
9
|
-
const {
|
|
9
|
+
const { File: NodeFile } = require('node:buffer')
|
|
10
10
|
|
|
11
11
|
const File = globalThis.File ?? NodeFile ?? UndiciFile
|
|
12
12
|
|
|
@@ -15,6 +15,18 @@ const filenameBuffer = Buffer.from('; filename')
|
|
|
15
15
|
const dd = Buffer.from('--')
|
|
16
16
|
const ddcrlf = Buffer.from('--\r\n')
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} chars
|
|
20
|
+
*/
|
|
21
|
+
function isAsciiString (chars) {
|
|
22
|
+
for (let i = 0; i < chars.length; ++i) {
|
|
23
|
+
if ((chars.charCodeAt(i) & ~0x7F) !== 0) {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
/**
|
|
19
31
|
* @see https://andreubotella.github.io/multipart-form-data/#multipart-form-data-boundary
|
|
20
32
|
* @param {string} boundary
|
|
@@ -30,7 +42,7 @@ function validateBoundary (boundary) {
|
|
|
30
42
|
// - it is composed by bytes in the ranges 0x30 to 0x39, 0x41 to 0x5A, or
|
|
31
43
|
// 0x61 to 0x7A, inclusive (ASCII alphanumeric), or which are 0x27 ('),
|
|
32
44
|
// 0x2D (-) or 0x5F (_).
|
|
33
|
-
for (let i = 0; i <
|
|
45
|
+
for (let i = 0; i < length; ++i) {
|
|
34
46
|
const cp = boundary.charCodeAt(i)
|
|
35
47
|
|
|
36
48
|
if (!(
|
|
@@ -58,12 +70,12 @@ function escapeFormDataName (name, encoding = 'utf-8', isFilename = false) {
|
|
|
58
70
|
// 1. If isFilename is true:
|
|
59
71
|
if (isFilename) {
|
|
60
72
|
// 1.1. Set name to the result of converting name into a scalar value string.
|
|
61
|
-
name =
|
|
73
|
+
name = toUSVString(name)
|
|
62
74
|
} else {
|
|
63
75
|
// 2. Otherwise:
|
|
64
76
|
|
|
65
77
|
// 2.1. Assert: name is a scalar value string.
|
|
66
|
-
assert(
|
|
78
|
+
assert(isUSVString(name))
|
|
67
79
|
|
|
68
80
|
// 2.2. Replace every occurrence of U+000D (CR) not followed by U+000A (LF),
|
|
69
81
|
// and every occurrence of U+000A (LF) not preceded by U+000D (CR), in
|
|
@@ -94,14 +106,16 @@ function multipartFormDataParser (input, mimeType) {
|
|
|
94
106
|
// 1. Assert: mimeType’s essence is "multipart/form-data".
|
|
95
107
|
assert(mimeType !== 'failure' && mimeType.essence === 'multipart/form-data')
|
|
96
108
|
|
|
109
|
+
const boundaryString = mimeType.parameters.get('boundary')
|
|
110
|
+
|
|
97
111
|
// 2. If mimeType’s parameters["boundary"] does not exist, return failure.
|
|
98
112
|
// Otherwise, let boundary be the result of UTF-8 decoding mimeType’s
|
|
99
113
|
// parameters["boundary"].
|
|
100
|
-
if (
|
|
114
|
+
if (boundaryString === undefined) {
|
|
101
115
|
return 'failure'
|
|
102
116
|
}
|
|
103
117
|
|
|
104
|
-
const boundary = Buffer.from(`--${
|
|
118
|
+
const boundary = Buffer.from(`--${boundaryString}`, 'utf8')
|
|
105
119
|
|
|
106
120
|
// 3. Let entry list be an empty entry list.
|
|
107
121
|
const entryList = []
|
|
@@ -200,7 +214,10 @@ function multipartFormDataParser (input, mimeType) {
|
|
|
200
214
|
contentType ??= 'text/plain'
|
|
201
215
|
|
|
202
216
|
// 5.10.2. If contentType is not an ASCII string, set contentType to the empty string.
|
|
203
|
-
|
|
217
|
+
|
|
218
|
+
// Note: `buffer.isAscii` can be used at zero-cost, but converting a string to a buffer is a high overhead.
|
|
219
|
+
// Content-Type is a relatively small string, so it is faster to use `String#charCodeAt`.
|
|
220
|
+
if (!isAsciiString(contentType)) {
|
|
204
221
|
contentType = ''
|
|
205
222
|
}
|
|
206
223
|
|
|
@@ -214,8 +231,8 @@ function multipartFormDataParser (input, mimeType) {
|
|
|
214
231
|
}
|
|
215
232
|
|
|
216
233
|
// 5.12. Assert: name is a scalar value string and value is either a scalar value string or a File object.
|
|
217
|
-
assert(
|
|
218
|
-
assert((typeof value === 'string' &&
|
|
234
|
+
assert(isUSVString(name))
|
|
235
|
+
assert((typeof value === 'string' && isUSVString(value)) || isFileLike(value))
|
|
219
236
|
|
|
220
237
|
// 5.13. Create an entry with name and value, and append it to entry list.
|
|
221
238
|
entryList.push(makeEntry(name, value, filename))
|
|
@@ -280,7 +297,7 @@ function parseMultipartFormDataHeaders (input, position) {
|
|
|
280
297
|
)
|
|
281
298
|
|
|
282
299
|
// 2.8. Byte-lowercase header name and switch on the result:
|
|
283
|
-
switch (
|
|
300
|
+
switch (bufferToLowerCasedHeaderName(headerName)) {
|
|
284
301
|
case 'content-disposition': {
|
|
285
302
|
// 1. Set name and filename to null.
|
|
286
303
|
name = filename = null
|
|
@@ -428,10 +445,9 @@ function parseMultipartFormDataName (input, position) {
|
|
|
428
445
|
*/
|
|
429
446
|
function collectASequenceOfBytes (condition, input, position) {
|
|
430
447
|
const result = []
|
|
431
|
-
let index = 0
|
|
432
448
|
|
|
433
449
|
while (position.position < input.length && condition(input[position.position])) {
|
|
434
|
-
result
|
|
450
|
+
result.push(input[position.position])
|
|
435
451
|
|
|
436
452
|
position.position++
|
|
437
453
|
}
|
package/lib/web/fetch/headers.js
CHANGED
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
} = require('./util')
|
|
13
13
|
const { webidl } = require('./webidl')
|
|
14
14
|
const assert = require('node:assert')
|
|
15
|
+
const util = require('util')
|
|
15
16
|
|
|
16
17
|
const kHeadersMap = Symbol('headers map')
|
|
17
18
|
const kHeadersSortedMap = Symbol('headers map sorted')
|
|
@@ -576,8 +577,18 @@ class Headers {
|
|
|
576
577
|
|
|
577
578
|
return this[kHeadersList]
|
|
578
579
|
}
|
|
580
|
+
|
|
581
|
+
[util.inspect.custom] (depth, options) {
|
|
582
|
+
const inspected = util.inspect(this[kHeadersList].entries)
|
|
583
|
+
|
|
584
|
+
return `Headers ${inspected}`
|
|
585
|
+
}
|
|
579
586
|
}
|
|
580
587
|
|
|
588
|
+
Object.defineProperty(Headers.prototype, util.inspect.custom, {
|
|
589
|
+
enumerable: false
|
|
590
|
+
})
|
|
591
|
+
|
|
581
592
|
iteratorMixin('Headers', Headers, kHeadersSortedMap, 0, 1)
|
|
582
593
|
|
|
583
594
|
Object.defineProperties(Headers.prototype, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "undici",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.8.0",
|
|
4
4
|
"description": "An HTTP/1.1 client, written from scratch for Node.js",
|
|
5
5
|
"homepage": "https://undici.nodejs.org",
|
|
6
6
|
"bugs": {
|
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
"jest": "^29.0.2",
|
|
108
108
|
"jsdom": "^24.0.0",
|
|
109
109
|
"jsfuzz": "^1.0.15",
|
|
110
|
+
"node-forge": "^1.3.1",
|
|
110
111
|
"pre-commit": "^1.2.2",
|
|
111
112
|
"proxy": "^2.1.1",
|
|
112
113
|
"snazzy": "^9.0.0",
|