@psnext/slingcli 2.4.20260525-3 → 2.4.20260526-1

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/README.md CHANGED
@@ -58,28 +58,37 @@ sling
58
58
 
59
59
  | Sling Command | Description |
60
60
  | ------------------- | ---------------------------------------- |
61
- | `/s:debug` | Toggle Slingshot debug logging |
62
- | `/s:footer` | Toggle custom footer with Slingshot info |
63
- | `/s:skills-manager | Open Skill Manager |
64
- | `/s:filechanges | Toggle file changes tracking |
65
- | `/s:context | Visualize context |
66
- | `/s:acm | Toggle Agentic Context Management |
61
+ | `/s:debug` | Toggle Slingshot debug logging |
62
+ | `/s:footer` | Toggle custom footer with Slingshot info |
63
+ | `/s:skills-manager` | Open Skill Manager |
64
+ | `/s:filechanges` | Toggle file changes tracking |
65
+ | `/s:context` | Visualize context |
66
+ | `/s:acm` | Toggle Agentic Context Management |
67
67
 
68
68
  ## Available models
69
69
 
70
70
  | Model | Reasoning |
71
71
  | -------------------------- | --------- |
72
- | claude-opus-4@20250514 | Yes |
73
- | claude-sonnet-4@20250514 | Yes |
74
- | claude-sonnet-4-5@20250929 | Yes |
72
+ | gpt-5.4-nano | Yes |
73
+ | gpt-4.1 | No |
75
74
  | gpt-5 | Yes |
75
+ | gpt-5-mini | Yes |
76
+ | gpt-5-nano | Yes |
76
77
  | gpt-5.1 | Yes |
77
78
  | gpt-5.2 | Yes |
79
+ | gpt-5.4-mini | Yes |
78
80
  | gpt-5.4 | Yes |
79
81
  | gpt-5.5 | Yes |
80
- | gpt-4.1 | No |
82
+ | gpt-5.2-codex | Yes |
83
+ | gpt-5.3-codex | Yes |
84
+ | claude-opus-4@20250514 | Yes |
85
+ | claude-sonnet-4@20250514 | Yes |
86
+ | claude-sonnet-4-5@20250929 | Yes |
87
+ | claude-sonnet-4-6 | Yes |
88
+ | claude-opus-4-6 | Yes |
81
89
  | o3 | Yes |
82
- | gemini-2.5-pro | Yes |
90
+ | o3-mini | Yes |
91
+ | o4-mini | Yes |
83
92
  | gemini-2.5-flash | Yes |
84
93
 
85
94
  ## Environment variables
package/bin/sling.js CHANGED
@@ -363,6 +363,8 @@ function updateSettings() {
363
363
  console.error("Failed to update settings.json:", err.message);
364
364
  }
365
365
  }
366
+ config.enableInstallTelemetry = false;
367
+
366
368
  config.powerline ={...{
367
369
  "preset": "default",
368
370
  "customItems": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babel/runtime",
3
- "version": "7.29.2",
3
+ "version": "7.29.7",
4
4
  "description": "babel's modular runtime helpers",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -7039,7 +7039,7 @@ export const MODELS = {
7039
7039
  cacheWrite: 0,
7040
7040
  },
7041
7041
  contextWindow: 200000,
7042
- maxTokens: 128000,
7042
+ maxTokens: 32000,
7043
7043
  },
7044
7044
  "claude-haiku-4-5": {
7045
7045
  id: "claude-haiku-4-5",
@@ -8498,13 +8498,13 @@ export const MODELS = {
8498
8498
  reasoning: false,
8499
8499
  input: ["text"],
8500
8500
  cost: {
8501
- input: 0.32,
8502
- output: 0.8899999999999999,
8501
+ input: 0.2288,
8502
+ output: 0.9144,
8503
8503
  cacheRead: 0,
8504
8504
  cacheWrite: 0,
8505
8505
  },
8506
- contextWindow: 163840,
8507
- maxTokens: 16384,
8506
+ contextWindow: 131072,
8507
+ maxTokens: 16000,
8508
8508
  },
8509
8509
  "deepseek/deepseek-chat-v3-0324": {
8510
8510
  id: "deepseek/deepseek-chat-v3-0324",
@@ -21,7 +21,7 @@ class RequestHandler extends AsyncResource {
21
21
  throw new InvalidArgumentError('invalid callback')
22
22
  }
23
23
 
24
- if (highWaterMark && (typeof highWaterMark !== 'number' || highWaterMark < 0)) {
24
+ if (highWaterMark != null && (!Number.isFinite(highWaterMark) || highWaterMark < 0)) {
25
25
  throw new InvalidArgumentError('invalid highWaterMark')
26
26
  }
27
27
 
@@ -216,7 +216,7 @@ module.exports = class SqliteCacheStore {
216
216
  SELECT
217
217
  id
218
218
  FROM cacheInterceptorV${VERSION}
219
- ORDER BY cachedAt DESC
219
+ ORDER BY cachedAt ASC
220
220
  LIMIT ?
221
221
  )
222
222
  `)
@@ -283,7 +283,6 @@ module.exports = class SqliteCacheStore {
283
283
  existingValue.id
284
284
  )
285
285
  } else {
286
- this.#prune()
287
286
  // New response, let's insert it
288
287
  this.#insertValueQuery.run(
289
288
  url,
@@ -299,6 +298,7 @@ module.exports = class SqliteCacheStore {
299
298
  value.cachedAt,
300
299
  value.staleAt
301
300
  )
301
+ this.#prune()
302
302
  }
303
303
  }
304
304
 
@@ -409,7 +409,7 @@ module.exports = class SqliteCacheStore {
409
409
  const now = Date.now()
410
410
  for (const value of values) {
411
411
  if (now >= value.deleteAt && !canBeExpired) {
412
- return undefined
412
+ continue
413
413
  }
414
414
 
415
415
  let matches = true
@@ -38,6 +38,22 @@ const SessionCache = class WeakSessionCache {
38
38
  return
39
39
  }
40
40
 
41
+ if (this._sessionCache.has(sessionKey)) {
42
+ this._sessionCache.delete(sessionKey)
43
+ } else if (this._sessionCache.size >= this._maxCachedSessions) {
44
+ for (const [key, ref] of this._sessionCache) {
45
+ if (ref.deref() === undefined) {
46
+ this._sessionCache.delete(key)
47
+ return
48
+ }
49
+ }
50
+
51
+ const oldest = this._sessionCache.keys().next()
52
+ if (!oldest.done) {
53
+ this._sessionCache.delete(oldest.value)
54
+ }
55
+ }
56
+
41
57
  this._sessionCache.set(sessionKey, new WeakRef(session))
42
58
  this._sessionRegistry.register(session, sessionKey)
43
59
  }
@@ -27,6 +27,21 @@ const { headerNameLowerCasedRecord } = require('./constants')
27
27
  // Verifies that a given path is valid does not contain control chars \x00 to \x20
28
28
  const invalidPathRegex = /[^\u0021-\u00ff]/
29
29
 
30
+ function isValidContentLengthHeaderValue (val) {
31
+ if (typeof val !== 'string' || val.length === 0) {
32
+ return false
33
+ }
34
+
35
+ for (let i = 0; i < val.length; i++) {
36
+ const charCode = val.charCodeAt(i)
37
+ if (charCode < 48 || charCode > 57) {
38
+ return false
39
+ }
40
+ }
41
+
42
+ return true
43
+ }
44
+
30
45
  const kHandler = Symbol('handler')
31
46
 
32
47
  class Request {
@@ -402,10 +417,10 @@ function processHeader (request, key, val) {
402
417
  if (request.contentLength !== null) {
403
418
  throw new InvalidArgumentError('duplicate content-length header')
404
419
  }
405
- request.contentLength = parseInt(val, 10)
406
- if (!Number.isFinite(request.contentLength)) {
420
+ if (!isValidContentLengthHeaderValue(val)) {
407
421
  throw new InvalidArgumentError('invalid content-length header')
408
422
  }
423
+ request.contentLength = parseInt(val, 10)
409
424
  } else if (request.contentType === null && headerName === 'content-type') {
410
425
  request.contentType = val
411
426
  request.headers.push(key, val)
@@ -7,6 +7,7 @@ const { debuglog } = require('node:util')
7
7
  const { parseAddress } = require('./socks5-utils')
8
8
 
9
9
  const debug = debuglog('undici:socks5')
10
+ const EMPTY_BUFFER = Buffer.alloc(0)
10
11
 
11
12
  // SOCKS5 constants
12
13
  const SOCKS_VERSION = 0x05
@@ -72,7 +73,10 @@ class Socks5Client extends EventEmitter {
72
73
  this.socket = socket
73
74
  this.options = options
74
75
  this.state = STATES.INITIAL
75
- this.buffer = Buffer.alloc(0)
76
+ this.buffer = EMPTY_BUFFER
77
+ this.onSocketData = this.onData.bind(this)
78
+ this.onSocketError = this.onError.bind(this)
79
+ this.onSocketClose = this.onClose.bind(this)
76
80
 
77
81
  // Authentication settings
78
82
  this.authMethods = []
@@ -82,9 +86,9 @@ class Socks5Client extends EventEmitter {
82
86
  this.authMethods.push(AUTH_METHODS.NO_AUTH)
83
87
 
84
88
  // Socket event handlers
85
- this.socket.on('data', this.onData.bind(this))
86
- this.socket.on('error', this.onError.bind(this))
87
- this.socket.on('close', this.onClose.bind(this))
89
+ this.socket.on('data', this.onSocketData)
90
+ this.socket.on('error', this.onSocketError)
91
+ this.socket.on('close', this.onSocketClose)
88
92
  }
89
93
 
90
94
  /**
@@ -363,8 +367,9 @@ class Socks5Client extends EventEmitter {
363
367
 
364
368
  const boundPort = this.buffer.readUInt16BE(offset)
365
369
 
366
- this.buffer = this.buffer.subarray(responseLength)
370
+ this.buffer = EMPTY_BUFFER
367
371
  this.state = STATES.CONNECTED
372
+ this.socket.removeListener('data', this.onSocketData)
368
373
 
369
374
  debug('connected, bound address:', boundAddress, 'port:', boundPort)
370
375
  this.emit('connected', { address: boundAddress, port: boundPort })
@@ -46,34 +46,29 @@ function parseAddress (address) {
46
46
  */
47
47
  function parseIPv6 (address) {
48
48
  const buffer = Buffer.alloc(16)
49
- const parts = address.split(':')
50
- let partIndex = 0
51
- let bufferIndex = 0
52
49
 
53
50
  // Handle compressed notation (::)
54
51
  const doubleColonIndex = address.indexOf('::')
55
52
  if (doubleColonIndex !== -1) {
56
- // Count non-empty parts
57
- const nonEmptyParts = parts.filter(p => p.length > 0).length
58
- const skipParts = 8 - nonEmptyParts
59
-
60
- for (let i = 0; i < parts.length; i++) {
61
- if (parts[i] === '' && i === doubleColonIndex / 3) {
62
- // Skip empty parts for ::
63
- bufferIndex += skipParts * 2
64
- } else if (parts[i] !== '') {
65
- const value = parseInt(parts[i], 16)
66
- buffer.writeUInt16BE(value, bufferIndex)
67
- bufferIndex += 2
68
- }
53
+ const before = address.slice(0, doubleColonIndex)
54
+ const after = address.slice(doubleColonIndex + 2)
55
+ const beforeParts = before === '' ? [] : before.split(':')
56
+ const afterParts = after === '' ? [] : after.split(':')
57
+
58
+ let bufferIndex = 0
59
+ for (const part of beforeParts) {
60
+ buffer.writeUInt16BE(parseInt(part, 16), bufferIndex)
61
+ bufferIndex += 2
62
+ }
63
+ bufferIndex = 16 - afterParts.length * 2
64
+ for (const part of afterParts) {
65
+ buffer.writeUInt16BE(parseInt(part, 16), bufferIndex)
66
+ bufferIndex += 2
69
67
  }
70
68
  } else {
71
- // No compression, parse normally
72
- for (const part of parts) {
73
- if (part === '') continue
74
- const value = parseInt(part, 16)
75
- buffer.writeUInt16BE(value, partIndex * 2)
76
- partIndex++
69
+ const parts = address.split(':')
70
+ for (let i = 0; i < parts.length; i++) {
71
+ buffer.writeUInt16BE(parseInt(parts[i], 16), i * 2)
77
72
  }
78
73
  }
79
74
 
@@ -69,9 +69,9 @@ function lazyllhttp () {
69
69
  let useWasmSIMD = process.arch !== 'ppc64'
70
70
  // The Env Variable UNDICI_NO_WASM_SIMD allows explicitly overriding the default behavior
71
71
  if (process.env.UNDICI_NO_WASM_SIMD === '1') {
72
- useWasmSIMD = true
73
- } else if (process.env.UNDICI_NO_WASM_SIMD === '0') {
74
72
  useWasmSIMD = false
73
+ } else if (process.env.UNDICI_NO_WASM_SIMD === '0') {
74
+ useWasmSIMD = true
75
75
  }
76
76
 
77
77
  if (useWasmSIMD) {
@@ -216,6 +216,7 @@ class Parser {
216
216
  */
217
217
  this.socket = socket
218
218
  this.timeout = null
219
+ this.timeoutWeakRef = new WeakRef(this)
219
220
  this.timeoutValue = null
220
221
  this.timeoutType = null
221
222
  this.statusCode = 0
@@ -253,9 +254,9 @@ class Parser {
253
254
 
254
255
  if (delay) {
255
256
  if (type & USE_FAST_TIMER) {
256
- this.timeout = timers.setFastTimeout(onParserTimeout, delay, new WeakRef(this))
257
+ this.timeout = timers.setFastTimeout(onParserTimeout, delay, this.timeoutWeakRef)
257
258
  } else {
258
- this.timeout = setTimeout(onParserTimeout, delay, new WeakRef(this))
259
+ this.timeout = setTimeout(onParserTimeout, delay, this.timeoutWeakRef)
259
260
  this.timeout?.unref()
260
261
  }
261
262
  }
@@ -349,16 +350,7 @@ class Parser {
349
350
  this.paused = true
350
351
  socket.unshift(data)
351
352
  } else {
352
- const ptr = llhttp.llhttp_get_error_reason(this.ptr)
353
- let message = ''
354
- if (ptr) {
355
- const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
356
- message =
357
- 'Response does not match the HTTP/1.1 protocol (' +
358
- Buffer.from(llhttp.memory.buffer, ptr, len).toString() +
359
- ')'
360
- }
361
- throw new HTTPParserError(message, constants.ERROR[ret], data)
353
+ throw this.createError(ret, data)
362
354
  }
363
355
  }
364
356
  } catch (err) {
@@ -366,6 +358,54 @@ class Parser {
366
358
  }
367
359
  }
368
360
 
361
+ finish () {
362
+ assert(currentParser === null)
363
+ assert(this.ptr != null)
364
+ assert(!this.paused)
365
+
366
+ const { llhttp } = this
367
+
368
+ let ret
369
+
370
+ try {
371
+ currentParser = this
372
+ ret = llhttp.llhttp_finish(this.ptr)
373
+ } finally {
374
+ currentParser = null
375
+ }
376
+
377
+ if (ret === constants.ERROR.OK) {
378
+ return null
379
+ }
380
+
381
+ if (ret === constants.ERROR.PAUSED || ret === constants.ERROR.PAUSED_UPGRADE) {
382
+ this.paused = true
383
+ return null
384
+ }
385
+
386
+ return this.createError(ret, EMPTY_BUF)
387
+ }
388
+
389
+ createError (ret, data) {
390
+ const { llhttp, contentLength, bytesRead } = this
391
+
392
+ if (contentLength && bytesRead !== parseInt(contentLength, 10)) {
393
+ return new ResponseContentLengthMismatchError()
394
+ }
395
+
396
+ const ptr = llhttp.llhttp_get_error_reason(this.ptr)
397
+ let message = ''
398
+ if (ptr) {
399
+ const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
400
+ message =
401
+ 'Response does not match the HTTP/1.1 protocol (' +
402
+ Buffer.from(llhttp.memory.buffer, ptr, len).toString() +
403
+ ')'
404
+ }
405
+
406
+ return new HTTPParserError(message, constants.ERROR[ret], data)
407
+ }
408
+
369
409
  destroy () {
370
410
  assert(currentParser === null)
371
411
  assert(this.ptr != null)
@@ -870,8 +910,11 @@ function onHttpSocketError (err) {
870
910
  // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
871
911
  // to the user.
872
912
  if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
873
- // We treat all incoming data so for as a valid response.
874
- parser.onMessageComplete()
913
+ const parserErr = parser.finish()
914
+ if (parserErr) {
915
+ this[kError] = parserErr
916
+ this[kClient][kOnError](parserErr)
917
+ }
875
918
  return
876
919
  }
877
920
 
@@ -888,8 +931,10 @@ function onHttpSocketEnd () {
888
931
  const parser = this[kParser]
889
932
 
890
933
  if (parser.statusCode && !parser.shouldKeepAlive) {
891
- // We treat all incoming data so far as a valid response.
892
- parser.onMessageComplete()
934
+ const parserErr = parser.finish()
935
+ if (parserErr) {
936
+ util.destroy(this, parserErr)
937
+ }
893
938
  return
894
939
  }
895
940
 
@@ -901,8 +946,7 @@ function onHttpSocketClose () {
901
946
 
902
947
  if (parser) {
903
948
  if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
904
- // We treat all incoming data so far as a valid response.
905
- parser.onMessageComplete()
949
+ this[kError] = parser.finish() || this[kError]
906
950
  }
907
951
 
908
952
  this[kParser].destroy()
@@ -235,9 +235,13 @@ class Client extends DispatcherBase {
235
235
  ...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
236
236
  ...connect
237
237
  })
238
- } else if (socketPath != null) {
238
+ } else {
239
239
  const customConnect = connect
240
- connect = (opts, callback) => customConnect({ ...opts, socketPath }, callback)
240
+ connect = (opts, callback) => customConnect({
241
+ ...opts,
242
+ ...(socketPath != null ? { socketPath } : null),
243
+ ...(allowH2 != null ? { allowH2 } : null)
244
+ }, callback)
241
245
  }
242
246
 
243
247
  this[kUrl] = util.parseOrigin(url)
@@ -15,7 +15,7 @@ class H2CClient extends Client {
15
15
  )
16
16
  }
17
17
 
18
- const { connect, maxConcurrentStreams, pipelining, ...opts } =
18
+ const { maxConcurrentStreams, pipelining, ...opts } =
19
19
  clientOpts ?? {}
20
20
  let defaultMaxConcurrentStreams = 100
21
21
  let defaultPipelining = 100
@@ -3,14 +3,14 @@
3
3
  const { kMockCallHistoryAddLog } = require('./mock-symbols')
4
4
  const { InvalidArgumentError } = require('../core/errors')
5
5
 
6
- function handleFilterCallsWithOptions (criteria, options, handler, store) {
6
+ function handleFilterCallsWithOptions (criteria, options, handler, store, allLogs) {
7
7
  switch (options.operator) {
8
8
  case 'OR':
9
- store.push(...handler(criteria))
9
+ store.push(...handler(criteria, allLogs))
10
10
 
11
11
  return store
12
12
  case 'AND':
13
- return handler.call({ logs: store }, criteria)
13
+ return handler(criteria, store)
14
14
  default:
15
15
  // guard -- should never happens because buildAndValidateFilterCallsOptions is called before
16
16
  throw new InvalidArgumentError('options.operator must to be a case insensitive string equal to \'OR\' or \'AND\'')
@@ -35,14 +35,14 @@ function buildAndValidateFilterCallsOptions (options = {}) {
35
35
  }
36
36
 
37
37
  function makeFilterCalls (parameterName) {
38
- return (parameterValue) => {
38
+ return (parameterValue, logs) => {
39
39
  if (typeof parameterValue === 'string' || parameterValue == null) {
40
- return this.logs.filter((log) => {
40
+ return logs.filter((log) => {
41
41
  return log[parameterName] === parameterValue
42
42
  })
43
43
  }
44
44
  if (parameterValue instanceof RegExp) {
45
- return this.logs.filter((log) => {
45
+ return logs.filter((log) => {
46
46
  return parameterValue.test(log[parameterName])
47
47
  })
48
48
  }
@@ -175,30 +175,30 @@ class MockCallHistory {
175
175
 
176
176
  const finalOptions = { operator: 'OR', ...buildAndValidateFilterCallsOptions(options) }
177
177
 
178
- let maybeDuplicatedLogsFiltered = []
178
+ let maybeDuplicatedLogsFiltered = finalOptions.operator === 'AND' ? this.logs : []
179
179
  if ('protocol' in criteria) {
180
- maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.protocol, finalOptions, this.filterCallsByProtocol, maybeDuplicatedLogsFiltered)
180
+ maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.protocol, finalOptions, this.filterCallsByProtocol, maybeDuplicatedLogsFiltered, this.logs)
181
181
  }
182
182
  if ('host' in criteria) {
183
- maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.host, finalOptions, this.filterCallsByHost, maybeDuplicatedLogsFiltered)
183
+ maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.host, finalOptions, this.filterCallsByHost, maybeDuplicatedLogsFiltered, this.logs)
184
184
  }
185
185
  if ('port' in criteria) {
186
- maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.port, finalOptions, this.filterCallsByPort, maybeDuplicatedLogsFiltered)
186
+ maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.port, finalOptions, this.filterCallsByPort, maybeDuplicatedLogsFiltered, this.logs)
187
187
  }
188
188
  if ('origin' in criteria) {
189
- maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.origin, finalOptions, this.filterCallsByOrigin, maybeDuplicatedLogsFiltered)
189
+ maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.origin, finalOptions, this.filterCallsByOrigin, maybeDuplicatedLogsFiltered, this.logs)
190
190
  }
191
191
  if ('path' in criteria) {
192
- maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.path, finalOptions, this.filterCallsByPath, maybeDuplicatedLogsFiltered)
192
+ maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.path, finalOptions, this.filterCallsByPath, maybeDuplicatedLogsFiltered, this.logs)
193
193
  }
194
194
  if ('hash' in criteria) {
195
- maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.hash, finalOptions, this.filterCallsByHash, maybeDuplicatedLogsFiltered)
195
+ maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.hash, finalOptions, this.filterCallsByHash, maybeDuplicatedLogsFiltered, this.logs)
196
196
  }
197
197
  if ('fullUrl' in criteria) {
198
- maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.fullUrl, finalOptions, this.filterCallsByFullUrl, maybeDuplicatedLogsFiltered)
198
+ maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.fullUrl, finalOptions, this.filterCallsByFullUrl, maybeDuplicatedLogsFiltered, this.logs)
199
199
  }
200
200
  if ('method' in criteria) {
201
- maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.method, finalOptions, this.filterCallsByMethod, maybeDuplicatedLogsFiltered)
201
+ maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.method, finalOptions, this.filterCallsByMethod, maybeDuplicatedLogsFiltered, this.logs)
202
202
  }
203
203
 
204
204
  const uniqLogsFiltered = [...new Set(maybeDuplicatedLogsFiltered)]
@@ -18,7 +18,7 @@ function makeCacheKey (opts) {
18
18
 
19
19
  let fullPath = opts.path || '/'
20
20
 
21
- if (opts.query && !pathHasQueryOrFragment(opts.path)) {
21
+ if (opts.query && !pathHasQueryOrFragment(fullPath)) {
22
22
  fullPath = serializePathWithQuery(fullPath, opts.query)
23
23
  }
24
24
 
@@ -374,9 +374,11 @@ function assertCacheMethods (methods, name = 'CacheMethods') {
374
374
  * @returns {string}
375
375
  */
376
376
  function makeDeduplicationKey (cacheKey, excludeHeaders) {
377
- // Create a deterministic string key from the cache key
378
- // Include origin, method, path, and sorted headers
379
- let key = `${cacheKey.origin}:${cacheKey.method}:${cacheKey.path}`
377
+ // Use JSON.stringify to produce a collision-resistant key.
378
+ // Previous format used `:` and `=` delimiters without escaping, which
379
+ // allowed different header sets to produce identical keys (e.g.
380
+ // {a:"x:b=y"} vs {a:"x", b:"y"}). See: https://github.com/nodejs/undici/issues/5012
381
+ const headers = {}
380
382
 
381
383
  if (cacheKey.headers) {
382
384
  const sortedHeaders = Object.keys(cacheKey.headers).sort()
@@ -385,12 +387,11 @@ function makeDeduplicationKey (cacheKey, excludeHeaders) {
385
387
  if (excludeHeaders?.has(header.toLowerCase())) {
386
388
  continue
387
389
  }
388
- const value = cacheKey.headers[header]
389
- key += `:${header}=${Array.isArray(value) ? value.join(',') : value}`
390
+ headers[header] = cacheKey.headers[header]
390
391
  }
391
392
  }
392
393
 
393
- return key
394
+ return JSON.stringify([cacheKey.origin, cacheKey.method, cacheKey.path, headers])
394
395
  }
395
396
 
396
397
  module.exports = {
@@ -204,7 +204,7 @@ function multipartFormDataParser (input, mimeType) {
204
204
  * Parses content-disposition attributes (e.g., name="value" or filename*=utf-8''encoded)
205
205
  * @param {Buffer} input
206
206
  * @param {{ position: number }} position
207
- * @returns {{ name: string, value: string }}
207
+ * @returns {{ name: string, value: string, extended: boolean } | null}
208
208
  */
209
209
  function parseContentDispositionAttribute (input, position) {
210
210
  // Skip leading semicolon and whitespace
@@ -304,7 +304,7 @@ function parseContentDispositionAttribute (input, position) {
304
304
  value = decoder.decode(tokenValue)
305
305
  }
306
306
 
307
- return { name: attrNameStr, value }
307
+ return { name: attrNameStr, value, extended: isExtended }
308
308
  }
309
309
 
310
310
  /**
@@ -368,6 +368,9 @@ function parseMultipartFormDataHeaders (input, position) {
368
368
  switch (bufferToLowerCasedHeaderName(headerName)) {
369
369
  case 'content-disposition': {
370
370
  name = filename = null
371
+ // Track whether filename was set from the extended (RFC 5987) form so
372
+ // a subsequent legacy `filename` attribute does not override it.
373
+ let filenameIsExtended = false
371
374
 
372
375
  // Collect the disposition type (should be "form-data")
373
376
  const dispositionType = collectASequenceOfBytes(
@@ -383,8 +386,8 @@ function parseMultipartFormDataHeaders (input, position) {
383
386
  // Parse attributes recursively until CRLF
384
387
  while (
385
388
  position.position < input.length &&
386
- input[position.position] !== 0x0d &&
387
- input[position.position + 1] !== 0x0a
389
+ (input[position.position] !== 0x0d ||
390
+ input[position.position + 1] !== 0x0a)
388
391
  ) {
389
392
  const attribute = parseContentDispositionAttribute(input, position)
390
393
 
@@ -395,7 +398,15 @@ function parseMultipartFormDataHeaders (input, position) {
395
398
  if (attribute.name === 'name') {
396
399
  name = attribute.value
397
400
  } else if (attribute.name === 'filename') {
398
- filename = attribute.value
401
+ // Per RFC 5987 §4.1, when both legacy and extended forms of the
402
+ // same parameter are present, the extended (filename*) form takes
403
+ // precedence regardless of the order they appear in.
404
+ if (attribute.extended) {
405
+ filename = attribute.value
406
+ filenameIsExtended = true
407
+ } else if (!filenameIsExtended) {
408
+ filename = attribute.value
409
+ }
399
410
  }
400
411
  }
401
412
 
@@ -448,7 +459,7 @@ function parseMultipartFormDataHeaders (input, position) {
448
459
 
449
460
  // 2.9. If position does not point to a sequence of bytes starting with 0x0D 0x0A
450
461
  // (CR LF), return failure. Otherwise, advance position by 2 (past the newline).
451
- if (input[position.position] !== 0x0d && input[position.position + 1] !== 0x0a) {
462
+ if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) {
452
463
  throw parsingError('expected CRLF')
453
464
  } else {
454
465
  position.position += 2
@@ -1030,7 +1030,7 @@ function fetchFinale (fetchParams, response) {
1030
1030
  let responseStatus = 0
1031
1031
 
1032
1032
  // 7. If fetchParams’s request’s mode is not "navigate" or response’s has-cross-origin-redirects is false:
1033
- if (fetchParams.request.mode !== 'navigator' || !response.hasCrossOriginRedirects) {
1033
+ if (fetchParams.request.mode !== 'navigate' || !response.hasCrossOriginRedirects) {
1034
1034
  // 1. Set responseStatus to response’s status.
1035
1035
  responseStatus = response.status
1036
1036
 
@@ -1433,7 +1433,10 @@ async function httpNetworkOrCacheFetch (
1433
1433
  // 8. If contentLengthHeaderValue is non-null, then append
1434
1434
  // `Content-Length`/contentLengthHeaderValue to httpRequest’s header
1435
1435
  // list.
1436
- if (contentLengthHeaderValue != null) {
1436
+ if (
1437
+ contentLengthHeaderValue != null &&
1438
+ !httpRequest.headersList.contains('content-length', true)
1439
+ ) {
1437
1440
  httpRequest.headersList.append('content-length', contentLengthHeaderValue, true)
1438
1441
  }
1439
1442