Files
edgeever/patches/npm@11.21.0.patch
T
tianma-if 59d6548abd fix: harden remaining dependency paths and desktop npm
Remove the unused React Native Markdown renderer. Pin safe YAML, shell quoting, tar, and cache dependencies; upgrade npm within the 11 series and apply upstream fixes to its hidden bundled Undici and cache copies.

Add actual-consumer security regressions and an opt-in real npm/ACP upgrade verifier. Typechecks, Web build, Android APK/signature checks, and 2,997 tests passed (23 skipped). Real old-to-new npm installs, ACP initialize, rollback, restart, and fallback retention passed under the desktop Electron runtime.

Android USB-device validation and full desktop automatic-update release gates remain pending. Dependency audit is reduced from 85 to 74 unique advisories; this is not a complete security clearance.
2026-10-12 12:48:47 +08:00

779 lines
24 KiB
Diff

diff --git a/node_modules/undici/lib/dispatcher/client-h1.js b/node_modules/undici/lib/dispatcher/client-h1.js
--- a/node_modules/undici/lib/dispatcher/client-h1.js
+++ b/node_modules/undici/lib/dispatcher/client-h1.js
@@ -876,7 +876,7 @@
function clearIdleSocketValidation (socket) {
if (socket[kIdleSocketValidationTimeout]) {
- clearTimeout(socket[kIdleSocketValidationTimeout])
+ clearImmediate(socket[kIdleSocketValidationTimeout])
socket[kIdleSocketValidationTimeout] = null
}
@@ -885,15 +885,23 @@
function scheduleIdleSocketValidation (client, socket) {
socket[kIdleSocketValidation] = 1
- socket[kIdleSocketValidationTimeout] = setTimeout(() => {
+ // Yield to the check phase (after poll) so unsolicited bytes / FIN / RST
+ // already pending on this idle keep-alive socket are processed before the
+ // next request is written (GHSA-35p6-xmwp-9g52).
+ //
+ // setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse
+ // (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll
+ // block for ~500ms when the event loop is otherwise idle (#5600 / #5606).
+ // A ref'd Immediate both keeps the pending request alive and makes poll
+ // return immediately — the hybrid those issues asked for.
+ socket[kIdleSocketValidationTimeout] = setImmediate(() => {
socket[kIdleSocketValidationTimeout] = null
socket[kIdleSocketValidation] = 2
if (client[kSocket] === socket && !socket.destroyed) {
client[kResume]()
}
- }, 0)
- socket[kIdleSocketValidationTimeout].unref?.()
+ })
}
/**
diff --git a/node_modules/undici/lib/web/websocket/connection.js b/node_modules/undici/lib/web/websocket/connection.js
--- a/node_modules/undici/lib/web/websocket/connection.js
+++ b/node_modules/undici/lib/web/websocket/connection.js
@@ -192,7 +192,7 @@
// is specified, the server needs to include the same field and one of
// the selected subprotocol values in its response for the connection to
// be established.
- if (!requestProtocols.includes(secProtocol)) {
+ if (requestProtocols === null || !requestProtocols.includes(secProtocol)) {
failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.')
return
}
diff --git a/node_modules/undici/lib/web/eventsource/eventsource-stream.js b/node_modules/undici/lib/web/eventsource/eventsource-stream.js
--- a/node_modules/undici/lib/web/eventsource/eventsource-stream.js
+++ b/node_modules/undici/lib/web/eventsource/eventsource-stream.js
@@ -22,6 +22,49 @@
* @type {32} SPACE
*/
const SPACE = 0x20
+
+const DATA = Buffer.from('data')
+const EVENT = Buffer.from('event')
+const ID = Buffer.from('id')
+const RETRY = Buffer.from('retry')
+
+function isASCIINumberBytes (buffer, start) {
+ if (start >= buffer.length) {
+ return false
+ }
+
+ for (let i = start; i < buffer.length; i++) {
+ if (buffer[i] < 0x30 || buffer[i] > 0x39) {
+ return false
+ }
+ }
+
+ return true
+}
+
+function isValidLastEventIdBytes (buffer, start) {
+ for (let i = start; i < buffer.length; i++) {
+ if (buffer[i] === 0x00) {
+ return false
+ }
+ }
+
+ return true
+}
+
+function isFieldName (line, length, field) {
+ if (length !== field.length) {
+ return false
+ }
+
+ for (let i = 0; i < length; i++) {
+ if (line[i] !== field[i]) {
+ return false
+ }
+ }
+
+ return true
+}
/**
* @typedef {object} EventSourceStreamEvent
@@ -63,11 +106,14 @@
eventEndCheck = false
/**
- * @type {Buffer}
- */
- buffer = null
-
+ * @type {Buffer[]}
+ */
+ chunks = []
+
+ chunkIndex = 0
pos = 0
+ lineChunkIndex = 0
+ linePos = 0
event = {
data: undefined,
@@ -106,92 +152,20 @@
return
}
- // Cache the chunk in the buffer, as the data might not be complete while
- // processing it
- // TODO: Investigate if there is a more performant way to handle
- // incoming chunks
- // see: https://github.com/nodejs/undici/issues/2630
- if (this.buffer) {
- this.buffer = Buffer.concat([this.buffer, chunk])
- } else {
- this.buffer = chunk
- }
+ this.chunks.push(chunk)
// Strip leading byte-order-mark if we opened the stream and started
// the processing of the incoming data
if (this.checkBOM) {
- switch (this.buffer.length) {
- case 1:
- // Check if the first byte is the same as the first byte of the BOM
- if (this.buffer[0] === BOM[0]) {
- // If it is, we need to wait for more data
- callback()
- return
- }
- // Set the checkBOM flag to false as we don't need to check for the
- // BOM anymore
- this.checkBOM = false
-
- // The buffer only contains one byte so we need to wait for more data
- callback()
- return
- case 2:
- // Check if the first two bytes are the same as the first two bytes
- // of the BOM
- if (
- this.buffer[0] === BOM[0] &&
- this.buffer[1] === BOM[1]
- ) {
- // If it is, we need to wait for more data, because the third byte
- // is needed to determine if it is the BOM or not
- callback()
- return
- }
-
- // Set the checkBOM flag to false as we don't need to check for the
- // BOM anymore
- this.checkBOM = false
- break
- case 3:
- // Check if the first three bytes are the same as the first three
- // bytes of the BOM
- if (
- this.buffer[0] === BOM[0] &&
- this.buffer[1] === BOM[1] &&
- this.buffer[2] === BOM[2]
- ) {
- // If it is, we can drop the buffered data, as it is only the BOM
- this.buffer = Buffer.alloc(0)
- // Set the checkBOM flag to false as we don't need to check for the
- // BOM anymore
- this.checkBOM = false
-
- // Await more data
- callback()
- return
- }
- // If it is not the BOM, we can start processing the data
- this.checkBOM = false
- break
- default:
- // The buffer is longer than 3 bytes, so we can drop the BOM if it is
- // present
- if (
- this.buffer[0] === BOM[0] &&
- this.buffer[1] === BOM[1] &&
- this.buffer[2] === BOM[2]
- ) {
- // Remove the BOM from the buffer
- this.buffer = this.buffer.subarray(3)
- }
-
- // Set the checkBOM flag to false as we don't need to check for the
- this.checkBOM = false
- break
- }
- }
-
- while (this.pos < this.buffer.length) {
+ if (this.handleBOM()) {
+ callback()
+ return
+ }
+ }
+
+ while (this.hasCurrentByte()) {
+ const byte = this.currentByte()
+
// If the previous line ended with an end-of-line, we need to check
// if the next character is also an end-of-line.
if (this.eventEndCheck) {
@@ -204,10 +178,9 @@
if (this.crlfCheck) {
// If the current character is a line feed, we can remove it
// from the buffer and reset the crlfCheck flag
- if (this.buffer[this.pos] === LF) {
- this.buffer = this.buffer.subarray(this.pos + 1)
- this.pos = 0
+ if (byte === LF) {
this.crlfCheck = false
+ this.consumeCurrentByte()
// It is possible that the line feed is not the end of the
// event. We need to check if the next character is an
@@ -223,19 +196,17 @@
this.crlfCheck = false
}
- if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
+ if (byte === LF || byte === CR) {
// If the current character is a carriage return, we need to
// set the crlfCheck flag to true, as we need to check if the
// next character is a line feed so we can remove it from the
// buffer
- if (this.buffer[this.pos] === CR) {
+ if (byte === CR) {
this.crlfCheck = true
}
- this.buffer = this.buffer.subarray(this.pos + 1)
- this.pos = 0
- if (
- this.event.data !== undefined || this.event.event || this.event.id || this.event.retry) {
+ this.consumeCurrentByte()
+ if (this.hasPendingEvent()) {
this.processEvent(this.event)
}
this.clearEvent()
@@ -249,22 +220,18 @@
// If the current character is an end-of-line, we can process the
// line
- if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
+ if (byte === LF || byte === CR) {
// If the current character is a carriage return, we need to
// set the crlfCheck flag to true, as we need to check if the
// next character is a line feed
- if (this.buffer[this.pos] === CR) {
+ if (byte === CR) {
this.crlfCheck = true
}
// In any case, we can process the line as we reached an
// end-of-line character
- this.parseLine(this.buffer.subarray(0, this.pos), this.event)
-
- // Remove the processed line from the buffer
- this.buffer = this.buffer.subarray(this.pos + 1)
- // Reset the position as we removed the processed line from the buffer
- this.pos = 0
+ this.parseLine(this.readLine(), this.event)
+ this.consumeCurrentByte()
// A line was processed and this could be the end of the event. We need
// to check if the next line is empty to determine if the event is
// finished.
@@ -272,7 +239,7 @@
continue
}
- this.pos++
+ this.advanceCursor()
}
callback()
@@ -297,64 +264,53 @@
return
}
- let field = ''
- let value = ''
+ let fieldLength = line.length
+ let valueStart = line.length
// If the line contains a U+003A COLON character (:)
if (colonPosition !== -1) {
- // Collect the characters on the line before the first U+003A COLON
- // character (:), and let field be that string.
- // TODO: Investigate if there is a more performant way to extract the
- // field
- // see: https://github.com/nodejs/undici/issues/2630
- field = line.subarray(0, colonPosition).toString('utf8')
+ fieldLength = colonPosition
// Collect the characters on the line after the first U+003A COLON
// character (:), and let value be that string.
// If value starts with a U+0020 SPACE character, remove it from value.
- let valueStart = colonPosition + 1
+ valueStart = colonPosition + 1
if (line[valueStart] === SPACE) {
++valueStart
}
- // TODO: Investigate if there is a more performant way to extract the
- // value
- // see: https://github.com/nodejs/undici/issues/2630
- value = line.subarray(valueStart).toString('utf8')
-
- // Otherwise, the string is not empty but does not contain a U+003A COLON
- // character (:)
- } else {
- // Process the field using the steps described below, using the whole
- // line as the field name, and the empty string as the field value.
- field = line.toString('utf8')
- value = ''
- }
-
- // Modify the event with the field name and value. The value is also
- // decoded as UTF-8
- switch (field) {
- case 'data':
- if (event[field] === undefined) {
- event[field] = value
- } else {
- event[field] += `\n${value}`
- }
- break
- case 'retry':
- if (isASCIINumber(value)) {
- event[field] = value
- }
- break
- case 'id':
- if (isValidLastEventId(value)) {
- event[field] = value
- }
- break
- case 'event':
- if (value.length > 0) {
- event[field] = value
- }
- break
+ }
+
+ if (isFieldName(line, fieldLength, DATA)) {
+ const value = line.toString('utf8', valueStart)
+
+ if (event.data === undefined) {
+ event.data = value
+ } else {
+ event.data += `\n${value}`
+ }
+ return
+ }
+
+ if (isFieldName(line, fieldLength, RETRY)) {
+ if (isASCIINumberBytes(line, valueStart)) {
+ event.retry = line.toString('utf8', valueStart)
+ }
+ return
+ }
+
+ if (isFieldName(line, fieldLength, ID)) {
+ if (isValidLastEventIdBytes(line, valueStart)) {
+ event.id = line.toString('utf8', valueStart)
+ }
+ return
+ }
+
+ if (isFieldName(line, fieldLength, EVENT)) {
+ const value = line.toString('utf8', valueStart)
+
+ if (value.length > 0) {
+ event.event = value
+ }
}
}
@@ -384,12 +340,151 @@
}
clearEvent () {
- this.event = {
- data: undefined,
- event: undefined,
- id: undefined,
- retry: undefined
- }
+ this.event.data = undefined
+ this.event.event = undefined
+ this.event.id = undefined
+ this.event.retry = undefined
+ }
+
+ hasPendingEvent () {
+ return this.event.data !== undefined ||
+ this.event.event !== undefined ||
+ this.event.id !== undefined ||
+ this.event.retry !== undefined
+ }
+
+ hasCurrentByte () {
+ return this.chunkIndex < this.chunks.length &&
+ this.pos < this.chunks[this.chunkIndex].length
+ }
+
+ currentByte () {
+ return this.chunks[this.chunkIndex][this.pos]
+ }
+
+ consumeCurrentByte () {
+ this.advanceCursor()
+ this.syncLineStartToCursor()
+ }
+
+ advanceCursor () {
+ this.pos++
+
+ while (this.chunkIndex < this.chunks.length && this.pos >= this.chunks[this.chunkIndex].length) {
+ this.chunkIndex++
+ this.pos = 0
+ }
+ }
+
+ syncLineStartToCursor () {
+ this.lineChunkIndex = this.chunkIndex
+ this.linePos = this.pos
+ this.dropConsumedChunks()
+ }
+
+ dropConsumedChunks () {
+ while (this.lineChunkIndex > 0) {
+ this.chunks.shift()
+ this.lineChunkIndex--
+ this.chunkIndex--
+ }
+
+ if (this.chunkIndex === this.chunks.length) {
+ this.chunks.length = 0
+ this.chunkIndex = 0
+ this.pos = 0
+ this.lineChunkIndex = 0
+ this.linePos = 0
+ }
+ }
+
+ readLine () {
+ if (this.lineChunkIndex === this.chunkIndex) {
+ return this.chunks[this.chunkIndex].subarray(this.linePos, this.pos)
+ }
+
+ const chunks = []
+ let length = 0
+
+ for (let i = this.lineChunkIndex; i <= this.chunkIndex; i++) {
+ const chunk = this.chunks[i]
+ const start = i === this.lineChunkIndex ? this.linePos : 0
+ const end = i === this.chunkIndex ? this.pos : chunk.length
+ const slice = chunk.subarray(start, end)
+ length += slice.length
+ chunks.push(slice)
+ }
+
+ return Buffer.concat(chunks, length)
+ }
+
+ peekBufferedByte (offset) {
+ let chunkIndex = this.lineChunkIndex
+ let pos = this.linePos
+
+ while (chunkIndex < this.chunks.length) {
+ const chunk = this.chunks[chunkIndex]
+ const remaining = chunk.length - pos
+
+ if (offset < remaining) {
+ return chunk[pos + offset]
+ }
+
+ offset -= remaining
+ chunkIndex++
+ pos = 0
+ }
+ }
+
+ discardLeadingBytes (count) {
+ while (count > 0 && this.lineChunkIndex < this.chunks.length) {
+ const chunk = this.chunks[this.lineChunkIndex]
+ const remaining = chunk.length - this.linePos
+
+ if (count < remaining) {
+ this.linePos += count
+ count = 0
+ } else {
+ count -= remaining
+ this.lineChunkIndex++
+ this.linePos = 0
+ }
+ }
+
+ this.chunkIndex = this.lineChunkIndex
+ this.pos = this.linePos
+ this.dropConsumedChunks()
+ }
+
+ handleBOM () {
+ const first = this.peekBufferedByte(0)
+ const second = this.peekBufferedByte(1)
+ const third = this.peekBufferedByte(2)
+
+ if (second === undefined) {
+ if (first === BOM[0]) {
+ return true
+ }
+
+ this.checkBOM = false
+ return true
+ }
+
+ if (third === undefined) {
+ if (first === BOM[0] && second === BOM[1]) {
+ return true
+ }
+
+ this.checkBOM = false
+ return false
+ }
+
+ if (first === BOM[0] && second === BOM[1] && third === BOM[2]) {
+ this.discardLeadingBytes(3)
+ }
+
+ this.checkBOM = false
+ return !this.hasCurrentByte()
}
}
diff --git a/node_modules/undici/lib/web/websocket/permessage-deflate.js b/node_modules/undici/lib/web/websocket/permessage-deflate.js
--- a/node_modules/undici/lib/web/websocket/permessage-deflate.js
+++ b/node_modules/undici/lib/web/websocket/permessage-deflate.js
@@ -63,7 +63,12 @@
if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) {
callback(new MessageSizeExceededError())
+ // The inflater may still hold buffered input that can emit a late
+ // zlib error. Remove the data listener, then deterministically stop
+ // the stream so a subsequent 'error' cannot fire without a listener
+ // (which would terminate the process as an unhandled error event).
this.#inflate.removeAllListeners()
+ this.#inflate.destroy()
this.#inflate = null
return
}
diff --git a/node_modules/undici/lib/handler/retry-handler.js b/node_modules/undici/lib/handler/retry-handler.js
--- a/node_modules/undici/lib/handler/retry-handler.js
+++ b/node_modules/undici/lib/handler/retry-handler.js
@@ -90,6 +90,7 @@
this.end = null
this.etag = null
this.resume = null
+ this.headersSent = false
// Handle possible onConnect duplication
this.handler.onConnect(reason => {
@@ -100,6 +101,20 @@
this.reason = reason
}
})
+ }
+
+ checkpointResponseEnd (headers, resume) {
+ if (this.end == null && this.opts.method !== 'HEAD') {
+ const contentLength = headers['content-length']
+ this.end = contentLength != null ? Number(contentLength) - 1 : null
+
+ assert(
+ this.end == null || Number.isFinite(this.end),
+ 'invalid content-length'
+ )
+ }
+
+ this.resume = this.end != null ? resume : null
}
onRequestSent () {
@@ -191,6 +206,8 @@
if (statusCode >= 300) {
if (this.retryOpts.statusCodes.includes(statusCode) === false) {
+ this.headersSent = true
+ this.checkpointResponseEnd(headers, resume)
return this.handler.onHeaders(
statusCode,
rawHeaders,
@@ -259,8 +276,15 @@
const { start, size, end = size - 1 } = contentRange
- assert(this.start === start, 'content-range mismatch')
- assert(this.end == null || this.end === end, 'content-range mismatch')
+ if (this.start !== start || (this.end != null && this.end !== end)) {
+ this.abort(
+ new RequestRetryError('Content-Range mismatch', statusCode, {
+ headers,
+ data: { count: this.retryCount }
+ })
+ )
+ return false
+ }
this.resume = resume
return true
@@ -272,6 +296,7 @@
const range = parseRangeHeader(headers['content-range'])
if (range == null) {
+ this.headersSent = true
return this.handler.onHeaders(
statusCode,
rawHeaders,
@@ -310,6 +335,7 @@
)
this.resume = resume
+ this.headersSent = true
this.etag = headers.etag != null ? headers.etag : null
// Weak etags are not useful for comparison nor cache
@@ -349,7 +375,7 @@
}
onError (err) {
- if (this.aborted || isDisturbed(this.opts.body)) {
+ if (this.aborted || isDisturbed(this.opts.body) || (this.headersSent && this.resume == null)) {
return this.handler.onError(err)
}
diff --git a/node_modules/undici/package.json b/node_modules/undici/package.json
--- a/node_modules/undici/package.json
+++ b/node_modules/undici/package.json
@@ -1,6 +1,6 @@
{
"name": "undici",
- "version": "6.28.0",
+ "version": "6.28.1",
"description": "An HTTP/1.1 client, written from scratch for Node.js",
"homepage": "https://undici.nodejs.org",
"bugs": {
diff --git a/node_modules/http-cache-semantics/index.js b/node_modules/http-cache-semantics/index.js
--- a/node_modules/http-cache-semantics/index.js
+++ b/node_modules/http-cache-semantics/index.js
@@ -334,12 +334,13 @@
/**
* @param {{headers: Record<string, string>, synchronous: boolean}|undefined} revalidation - Revalidation information, if any.
- * @returns {{response: {headers: Record<string, string>}, revalidation: {headers: Record<string, string>, synchronous: boolean}|undefined}} An object with a cached response headers and revalidation info.
+ * @returns {{response: HttpResponse, revalidation: {headers: Record<string, string>, synchronous: boolean}|undefined}} An object with a cached response headers and revalidation info.
*/
_evaluateRequestHitResult(revalidation) {
return {
response: {
headers: this.responseHeaders(),
+ status: this._status,
},
revalidation,
};
@@ -387,9 +388,9 @@
* }
* ```
* @param {HttpRequest} req - new incoming HTTP request
- * @returns {{response: {headers: Record<string, string>}|undefined, revalidation: {headers: Record<string, string>, synchronous: boolean}|undefined}} An object containing keys:
+ * @returns {{response: HttpResponse|undefined, revalidation: {headers: Record<string, string>, synchronous: boolean}|undefined}} An object containing keys:
* - revalidation: { headers: Record<string, string>, synchronous: boolean } Set if you should send this to the origin server
- * - response: { headers: Record<string, string> } Set if you can respond to the client with these cached headers
+ * - response: HttpResponse Set if you can respond to the client with these cached headers
*/
evaluateRequest(req) {
this._assertRequestHasHeaders(req);
@@ -484,18 +485,32 @@
return true;
}
- // A Vary header field-value of "*" always fails to match
- if (this._resHeaders.vary === '*') {
- return false;
- }
-
const fields = this._resHeaders.vary
.trim()
.toLowerCase()
.split(/\s*,\s*/);
+
for (const name of fields) {
- if (req.headers[name] !== this._reqHeaders[name]) return false;
- }
+ // A Vary header field-value of "*" always fails to match
+ if (name === '*') {
+ return false;
+ }
+
+ const newReq = Object.prototype.hasOwnProperty.call(
+ req.headers,
+ name
+ ) && req.headers[name];
+
+ const cachedReq = Object.prototype.hasOwnProperty.call(
+ this._reqHeaders,
+ name
+ ) && this._reqHeaders[name];
+
+ if (newReq !== cachedReq) {
+ return false;
+ }
+ }
+
return true;
}
@@ -554,6 +569,14 @@
headers.age = `${Math.round(age)}`;
headers.date = new Date(this.now()).toUTCString();
return headers;
+ }
+
+ /**
+ * Returns the status code of the cached response.
+ * @returns {number} The response status code.
+ */
+ status() {
+ return this._status;
}
/**
diff --git a/node_modules/http-cache-semantics/package.json b/node_modules/http-cache-semantics/package.json
--- a/node_modules/http-cache-semantics/package.json
+++ b/node_modules/http-cache-semantics/package.json
@@ -1,6 +1,6 @@
{
"name": "http-cache-semantics",
- "version": "4.2.0",
+ "version": "4.3.0",
"description": "Parses Cache-Control and other headers. Helps building correct HTTP caches and proxies",
"repository": {
"type": "git",
@@ -14,9 +14,9 @@
"files": [
"index.js"
],
- "author": "Kornel Lesiński <npms2@geekhood.net> (https://kornel.ski/)",
+ "author": "Kornel Lesiński <kornel@geekhood.net> (https://kornel.ski/)",
"license": "BSD-2-Clause",
"devDependencies": {
- "mocha": "^11.0"
+ "mocha": "^12.0"
}
}