faf-taf-git 2.2.2 → 2.3.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/dist/index.js CHANGED
@@ -7245,7 +7245,13 @@ function processHeader (request, key, val) {
7245
7245
  } else if (typeof val[i] === 'object') {
7246
7246
  throw new InvalidArgumentError(`invalid ${key} header`)
7247
7247
  } else {
7248
- arr.push(`${val[i]}`)
7248
+ // Coerce primitives (and reject unsafe coercions such as functions
7249
+ // with a crafted toString/Symbol.toPrimitive).
7250
+ const str = `${val[i]}`
7251
+ if (!isValidHeaderValue(str)) {
7252
+ throw new InvalidArgumentError(`invalid ${key} header`)
7253
+ }
7254
+ arr.push(str)
7249
7255
  }
7250
7256
  }
7251
7257
  val = arr
@@ -7256,7 +7262,12 @@ function processHeader (request, key, val) {
7256
7262
  } else if (val === null) {
7257
7263
  val = ''
7258
7264
  } else {
7265
+ // Coerce primitives (and reject unsafe coercions such as functions
7266
+ // with a crafted toString/Symbol.toPrimitive).
7259
7267
  val = `${val}`
7268
+ if (!isValidHeaderValue(val)) {
7269
+ throw new InvalidArgumentError(`invalid ${key} header`)
7270
+ }
7260
7271
  }
7261
7272
 
7262
7273
  if (headerName === 'host') {
@@ -8293,8 +8304,6 @@ function defaultFactory (origin, opts) {
8293
8304
 
8294
8305
  class Agent extends DispatcherBase {
8295
8306
  constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) {
8296
- super()
8297
-
8298
8307
  if (typeof factory !== 'function') {
8299
8308
  throw new InvalidArgumentError('factory must be a function.')
8300
8309
  }
@@ -8307,6 +8316,8 @@ class Agent extends DispatcherBase {
8307
8316
  throw new InvalidArgumentError('maxRedirections must be a positive number')
8308
8317
  }
8309
8318
 
8319
+ super(options)
8320
+
8310
8321
  if (connect && typeof connect !== 'function') {
8311
8322
  connect = { ...connect }
8312
8323
  }
@@ -8633,6 +8644,7 @@ const {
8633
8644
  RequestContentLengthMismatchError,
8634
8645
  ResponseContentLengthMismatchError,
8635
8646
  RequestAbortedError,
8647
+ InvalidArgumentError,
8636
8648
  HeadersTimeoutError,
8637
8649
  HeadersOverflowError,
8638
8650
  SocketError,
@@ -8680,6 +8692,9 @@ const EMPTY_BUF = Buffer.alloc(0)
8680
8692
  const FastBuffer = Buffer[Symbol.species]
8681
8693
  const addListener = util.addListener
8682
8694
  const removeAllListeners = util.removeAllListeners
8695
+ const kIdleSocketValidation = Symbol('kIdleSocketValidation')
8696
+ const kIdleSocketValidationTimeout = Symbol('kIdleSocketValidationTimeout')
8697
+ const kSocketUsed = Symbol('kSocketUsed')
8683
8698
 
8684
8699
  let extractBody
8685
8700
 
@@ -8902,29 +8917,71 @@ class Parser {
8902
8917
 
8903
8918
  const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr
8904
8919
 
8905
- if (ret === constants.ERROR.PAUSED_UPGRADE) {
8906
- this.onUpgrade(data.slice(offset))
8907
- } else if (ret === constants.ERROR.PAUSED) {
8908
- this.paused = true
8909
- socket.unshift(data.slice(offset))
8910
- } else if (ret !== constants.ERROR.OK) {
8911
- const ptr = llhttp.llhttp_get_error_reason(this.ptr)
8912
- let message = ''
8913
- /* istanbul ignore else: difficult to make a test case for */
8914
- if (ptr) {
8915
- const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
8916
- message =
8917
- 'Response does not match the HTTP/1.1 protocol (' +
8918
- Buffer.from(llhttp.memory.buffer, ptr, len).toString() +
8919
- ')'
8920
- }
8921
- throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset))
8920
+ if (ret !== constants.ERROR.OK) {
8921
+ const body = data.subarray(offset)
8922
+
8923
+ if (ret === constants.ERROR.PAUSED_UPGRADE) {
8924
+ this.onUpgrade(body)
8925
+ } else if (ret === constants.ERROR.PAUSED) {
8926
+ this.paused = true
8927
+ socket.unshift(body)
8928
+ } else {
8929
+ throw this.createError(ret, body)
8930
+ }
8922
8931
  }
8923
8932
  } catch (err) {
8924
8933
  util.destroy(socket, err)
8925
8934
  }
8926
8935
  }
8927
8936
 
8937
+ finish () {
8938
+ assert(currentParser === null)
8939
+ assert(this.ptr != null)
8940
+ assert(!this.paused)
8941
+
8942
+ const { llhttp } = this
8943
+
8944
+ let ret
8945
+
8946
+ try {
8947
+ currentParser = this
8948
+ ret = llhttp.llhttp_finish(this.ptr)
8949
+ } finally {
8950
+ currentParser = null
8951
+ }
8952
+
8953
+ if (ret === constants.ERROR.OK) {
8954
+ return null
8955
+ }
8956
+
8957
+ if (ret === constants.ERROR.PAUSED || ret === constants.ERROR.PAUSED_UPGRADE) {
8958
+ this.paused = true
8959
+ return null
8960
+ }
8961
+
8962
+ return this.createError(ret, EMPTY_BUF)
8963
+ }
8964
+
8965
+ createError (ret, data) {
8966
+ const { llhttp, contentLength, bytesRead } = this
8967
+
8968
+ if (contentLength && bytesRead !== parseInt(contentLength, 10)) {
8969
+ return new ResponseContentLengthMismatchError()
8970
+ }
8971
+
8972
+ const ptr = llhttp.llhttp_get_error_reason(this.ptr)
8973
+ let message = ''
8974
+ if (ptr) {
8975
+ const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
8976
+ message =
8977
+ 'Response does not match the HTTP/1.1 protocol (' +
8978
+ Buffer.from(llhttp.memory.buffer, ptr, len).toString() +
8979
+ ')'
8980
+ }
8981
+
8982
+ return new HTTPParserError(message, constants.ERROR[ret], data)
8983
+ }
8984
+
8928
8985
  destroy () {
8929
8986
  assert(this.ptr != null)
8930
8987
  assert(currentParser == null)
@@ -8952,6 +9009,11 @@ class Parser {
8952
9009
  return -1
8953
9010
  }
8954
9011
 
9012
+ if (client[kRunning] === 0) {
9013
+ util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
9014
+ return -1
9015
+ }
9016
+
8955
9017
  const request = client[kQueue][client[kRunningIdx]]
8956
9018
  if (!request) {
8957
9019
  return -1
@@ -9055,6 +9117,11 @@ class Parser {
9055
9117
  return -1
9056
9118
  }
9057
9119
 
9120
+ if (client[kRunning] === 0) {
9121
+ util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
9122
+ return -1
9123
+ }
9124
+
9058
9125
  const request = client[kQueue][client[kRunningIdx]]
9059
9126
 
9060
9127
  /* istanbul ignore next: difficult to make a test case for */
@@ -9228,6 +9295,7 @@ class Parser {
9228
9295
  request.onComplete(headers)
9229
9296
 
9230
9297
  client[kQueue][client[kRunningIdx]++] = null
9298
+ socket[kSocketUsed] = true
9231
9299
 
9232
9300
  if (socket[kWriting]) {
9233
9301
  assert(client[kRunning] === 0)
@@ -9286,6 +9354,9 @@ async function connectH1 (client, socket) {
9286
9354
  socket[kWriting] = false
9287
9355
  socket[kReset] = false
9288
9356
  socket[kBlocking] = false
9357
+ socket[kIdleSocketValidation] = 0
9358
+ socket[kIdleSocketValidationTimeout] = null
9359
+ socket[kSocketUsed] = false
9289
9360
  socket[kParser] = new Parser(client, socket, llhttpInstance)
9290
9361
 
9291
9362
  addListener(socket, 'error', function (err) {
@@ -9296,8 +9367,11 @@ async function connectH1 (client, socket) {
9296
9367
  // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
9297
9368
  // to the user.
9298
9369
  if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
9299
- // We treat all incoming data so for as a valid response.
9300
- parser.onMessageComplete()
9370
+ const parserErr = parser.finish()
9371
+ if (parserErr) {
9372
+ this[kError] = parserErr
9373
+ this[kClient][kOnError](parserErr)
9374
+ }
9301
9375
  return
9302
9376
  }
9303
9377
 
@@ -9316,8 +9390,10 @@ async function connectH1 (client, socket) {
9316
9390
  const parser = this[kParser]
9317
9391
 
9318
9392
  if (parser.statusCode && !parser.shouldKeepAlive) {
9319
- // We treat all incoming data so far as a valid response.
9320
- parser.onMessageComplete()
9393
+ const parserErr = parser.finish()
9394
+ if (parserErr) {
9395
+ util.destroy(this, parserErr)
9396
+ }
9321
9397
  return
9322
9398
  }
9323
9399
 
@@ -9327,10 +9403,11 @@ async function connectH1 (client, socket) {
9327
9403
  const client = this[kClient]
9328
9404
  const parser = this[kParser]
9329
9405
 
9406
+ clearIdleSocketValidation(this)
9407
+
9330
9408
  if (parser) {
9331
9409
  if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
9332
- // We treat all incoming data so far as a valid response.
9333
- parser.onMessageComplete()
9410
+ this[kError] = parser.finish() || this[kError]
9334
9411
  }
9335
9412
 
9336
9413
  this[kParser].destroy()
@@ -9393,7 +9470,7 @@ async function connectH1 (client, socket) {
9393
9470
  return socket.destroyed
9394
9471
  },
9395
9472
  busy (request) {
9396
- if (socket[kWriting] || socket[kReset] || socket[kBlocking]) {
9473
+ if (socket[kWriting] || socket[kReset] || socket[kBlocking] || socket[kIdleSocketValidation] === 1) {
9397
9474
  return true
9398
9475
  }
9399
9476
 
@@ -9431,6 +9508,39 @@ async function connectH1 (client, socket) {
9431
9508
  }
9432
9509
  }
9433
9510
 
9511
+ function clearIdleSocketValidation (socket) {
9512
+ if (socket[kIdleSocketValidationTimeout]) {
9513
+ clearImmediate(socket[kIdleSocketValidationTimeout])
9514
+ socket[kIdleSocketValidationTimeout] = null
9515
+ }
9516
+
9517
+ socket[kIdleSocketValidation] = 0
9518
+ }
9519
+
9520
+ function scheduleIdleSocketValidation (client, socket) {
9521
+ socket[kIdleSocketValidation] = 1
9522
+ // Yield to the check phase (after poll) so unsolicited bytes / FIN / RST
9523
+ // already pending on this idle keep-alive socket are processed before the
9524
+ // next request is written (GHSA-35p6-xmwp-9g52).
9525
+ //
9526
+ // setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse
9527
+ // (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll
9528
+ // block for ~500ms when the event loop is otherwise idle (#5600 / #5606).
9529
+ // A ref'd Immediate both keeps the pending request alive and makes poll
9530
+ // return immediately — the hybrid those issues asked for.
9531
+ socket[kIdleSocketValidationTimeout] = setImmediate(() => {
9532
+ socket[kIdleSocketValidationTimeout] = null
9533
+ socket[kIdleSocketValidation] = 2
9534
+
9535
+ if (client[kSocket] === socket && !socket.destroyed) {
9536
+ client[kResume]()
9537
+ }
9538
+ })
9539
+ }
9540
+
9541
+ /**
9542
+ * @param {import('./client.js')} client
9543
+ */
9434
9544
  function resumeH1 (client) {
9435
9545
  const socket = client[kSocket]
9436
9546
 
@@ -9445,6 +9555,32 @@ function resumeH1 (client) {
9445
9555
  socket[kNoRef] = false
9446
9556
  }
9447
9557
 
9558
+ if (client[kRunning] === 0 && client[kPending] > 0 && socket[kSocketUsed]) {
9559
+ if (socket[kIdleSocketValidation] === 0) {
9560
+ scheduleIdleSocketValidation(client, socket)
9561
+ socket[kParser].readMore()
9562
+ if (socket.destroyed) {
9563
+ return
9564
+ }
9565
+ return
9566
+ }
9567
+
9568
+ if (socket[kIdleSocketValidation] === 1) {
9569
+ socket[kParser].readMore()
9570
+ if (socket.destroyed) {
9571
+ return
9572
+ }
9573
+ return
9574
+ }
9575
+ }
9576
+
9577
+ if (client[kRunning] === 0) {
9578
+ socket[kParser].readMore()
9579
+ if (socket.destroyed) {
9580
+ return
9581
+ }
9582
+ }
9583
+
9448
9584
  if (client[kSize] === 0) {
9449
9585
  if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) {
9450
9586
  socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE)
@@ -9500,8 +9636,16 @@ function writeH1 (client, request) {
9500
9636
  }
9501
9637
  body = bodyStream.stream
9502
9638
  contentLength = bodyStream.length
9503
- } else if (util.isBlobLike(body) && request.contentType == null && body.type) {
9504
- headers.push('content-type', body.type)
9639
+ } else if (util.isBlobLike(body) && request.contentType == null) {
9640
+ const contentType = body.type
9641
+ if (contentType) {
9642
+ const contentTypeValue = `${contentType}`
9643
+ if (!util.isValidHeaderValue(contentTypeValue)) {
9644
+ util.errorRequest(client, request, new InvalidArgumentError('invalid content-type header'))
9645
+ return false
9646
+ }
9647
+ headers.push('content-type', contentTypeValue)
9648
+ }
9505
9649
  }
9506
9650
 
9507
9651
  if (body && typeof body.read === 'function') {
@@ -9538,6 +9682,7 @@ function writeH1 (client, request) {
9538
9682
  }
9539
9683
 
9540
9684
  const socket = client[kSocket]
9685
+ clearIdleSocketValidation(socket)
9541
9686
 
9542
9687
  const abort = (err) => {
9543
9688
  if (request.aborted || request.completed) {
@@ -10859,9 +11004,10 @@ class Client extends DispatcherBase {
10859
11004
  autoSelectFamilyAttemptTimeout,
10860
11005
  // h2
10861
11006
  maxConcurrentStreams,
10862
- allowH2
11007
+ allowH2,
11008
+ webSocket
10863
11009
  } = {}) {
10864
- super()
11010
+ super({ webSocket })
10865
11011
 
10866
11012
  if (keepAlive !== undefined) {
10867
11013
  throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
@@ -11394,15 +11540,24 @@ const { kDestroy, kClose, kClosed, kDestroyed, kDispatch, kInterceptors } = __nc
11394
11540
  const kOnDestroyed = Symbol('onDestroyed')
11395
11541
  const kOnClosed = Symbol('onClosed')
11396
11542
  const kInterceptedDispatch = Symbol('Intercepted Dispatch')
11543
+ const kWebSocketOptions = Symbol('webSocketOptions')
11397
11544
 
11398
11545
  class DispatcherBase extends Dispatcher {
11399
- constructor () {
11546
+ constructor (opts) {
11400
11547
  super()
11401
11548
 
11402
11549
  this[kDestroyed] = false
11403
11550
  this[kOnDestroyed] = null
11404
11551
  this[kClosed] = false
11405
11552
  this[kOnClosed] = []
11553
+ this[kWebSocketOptions] = opts?.webSocket ?? {}
11554
+ }
11555
+
11556
+ get webSocketOptions () {
11557
+ return {
11558
+ maxFragments: this[kWebSocketOptions].maxFragments ?? 131072,
11559
+ maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024
11560
+ }
11406
11561
  }
11407
11562
 
11408
11563
  get destroyed () {
@@ -11966,8 +12121,8 @@ const kRemoveClient = Symbol('remove client')
11966
12121
  const kStats = Symbol('stats')
11967
12122
 
11968
12123
  class PoolBase extends DispatcherBase {
11969
- constructor () {
11970
- super()
12124
+ constructor (opts) {
12125
+ super(opts)
11971
12126
 
11972
12127
  this[kQueue] = new FixedQueue()
11973
12128
  this[kClients] = []
@@ -12227,8 +12382,6 @@ class Pool extends PoolBase {
12227
12382
  allowH2,
12228
12383
  ...options
12229
12384
  } = {}) {
12230
- super()
12231
-
12232
12385
  if (connections != null && (!Number.isFinite(connections) || connections < 0)) {
12233
12386
  throw new InvalidArgumentError('invalid connections')
12234
12387
  }
@@ -12253,6 +12406,8 @@ class Pool extends PoolBase {
12253
12406
  })
12254
12407
  }
12255
12408
 
12409
+ super(options)
12410
+
12256
12411
  this[kInterceptors] = options.interceptors?.Pool && Array.isArray(options.interceptors.Pool)
12257
12412
  ? options.interceptors.Pool
12258
12413
  : []
@@ -12977,6 +13132,28 @@ function calculateRetryAfterHeader (retryAfter) {
12977
13132
  return new Date(retryAfter).getTime() - current
12978
13133
  }
12979
13134
 
13135
+ function validatePartialResponseContentLength (headers, range, statusCode, retryCount) {
13136
+ const contentLength = headers['content-length']
13137
+ if (contentLength == null) {
13138
+ return null
13139
+ }
13140
+
13141
+ if (!Number.isFinite(range.start) || !Number.isFinite(range.end)) {
13142
+ return null
13143
+ }
13144
+
13145
+ const length = Number(contentLength)
13146
+ const expectedLength = range.end - range.start + 1
13147
+ if (!Number.isFinite(length) || length !== expectedLength) {
13148
+ return new RequestRetryError('Content-Length mismatch', statusCode, {
13149
+ headers,
13150
+ data: { count: retryCount }
13151
+ })
13152
+ }
13153
+
13154
+ return null
13155
+ }
13156
+
12980
13157
  class RetryHandler {
12981
13158
  constructor (opts, handlers) {
12982
13159
  const { retryOptions, ...dispatchOpts } = opts
@@ -13030,6 +13207,7 @@ class RetryHandler {
13030
13207
  this.end = null
13031
13208
  this.etag = null
13032
13209
  this.resume = null
13210
+ this.headersSent = false
13033
13211
 
13034
13212
  // Handle possible onConnect duplication
13035
13213
  this.handler.onConnect(reason => {
@@ -13042,6 +13220,20 @@ class RetryHandler {
13042
13220
  })
13043
13221
  }
13044
13222
 
13223
+ checkpointResponseEnd (headers, resume) {
13224
+ if (this.end == null && this.opts.method !== 'HEAD') {
13225
+ const contentLength = headers['content-length']
13226
+ this.end = contentLength != null ? Number(contentLength) - 1 : null
13227
+
13228
+ assert(
13229
+ this.end == null || Number.isFinite(this.end),
13230
+ 'invalid content-length'
13231
+ )
13232
+ }
13233
+
13234
+ this.resume = this.end != null ? resume : null
13235
+ }
13236
+
13045
13237
  onRequestSent () {
13046
13238
  if (this.handler.onRequestSent) {
13047
13239
  this.handler.onRequestSent()
@@ -13131,6 +13323,8 @@ class RetryHandler {
13131
13323
 
13132
13324
  if (statusCode >= 300) {
13133
13325
  if (this.retryOpts.statusCodes.includes(statusCode) === false) {
13326
+ this.headersSent = true
13327
+ this.checkpointResponseEnd(headers, resume)
13134
13328
  return this.handler.onHeaders(
13135
13329
  statusCode,
13136
13330
  rawHeaders,
@@ -13191,10 +13385,23 @@ class RetryHandler {
13191
13385
  return false
13192
13386
  }
13193
13387
 
13388
+ const contentLengthError = validatePartialResponseContentLength(headers, contentRange, statusCode, this.retryCount)
13389
+ if (contentLengthError != null) {
13390
+ this.abort(contentLengthError)
13391
+ return false
13392
+ }
13393
+
13194
13394
  const { start, size, end = size - 1 } = contentRange
13195
13395
 
13196
- assert(this.start === start, 'content-range mismatch')
13197
- assert(this.end == null || this.end === end, 'content-range mismatch')
13396
+ if (this.start !== start || (this.end != null && this.end !== end)) {
13397
+ this.abort(
13398
+ new RequestRetryError('Content-Range mismatch', statusCode, {
13399
+ headers,
13400
+ data: { count: this.retryCount }
13401
+ })
13402
+ )
13403
+ return false
13404
+ }
13198
13405
 
13199
13406
  this.resume = resume
13200
13407
  return true
@@ -13206,6 +13413,7 @@ class RetryHandler {
13206
13413
  const range = parseRangeHeader(headers['content-range'])
13207
13414
 
13208
13415
  if (range == null) {
13416
+ this.headersSent = true
13209
13417
  return this.handler.onHeaders(
13210
13418
  statusCode,
13211
13419
  rawHeaders,
@@ -13214,6 +13422,12 @@ class RetryHandler {
13214
13422
  )
13215
13423
  }
13216
13424
 
13425
+ const contentLengthError = validatePartialResponseContentLength(headers, range, statusCode, this.retryCount)
13426
+ if (contentLengthError != null) {
13427
+ this.abort(contentLengthError)
13428
+ return false
13429
+ }
13430
+
13217
13431
  const { start, size, end = size - 1 } = range
13218
13432
  assert(
13219
13433
  start != null && Number.isFinite(start),
@@ -13238,6 +13452,7 @@ class RetryHandler {
13238
13452
  )
13239
13453
 
13240
13454
  this.resume = resume
13455
+ this.headersSent = true
13241
13456
  this.etag = headers.etag != null ? headers.etag : null
13242
13457
 
13243
13458
  // Weak etags are not useful for comparison nor cache
@@ -13277,7 +13492,7 @@ class RetryHandler {
13277
13492
  }
13278
13493
 
13279
13494
  onError (err) {
13280
- if (this.aborted || isDisturbed(this.opts.body)) {
13495
+ if (this.aborted || isDisturbed(this.opts.body) || (this.headersSent && this.resume == null)) {
13281
13496
  return this.handler.onError(err)
13282
13497
  }
13283
13498
 
@@ -17337,32 +17552,25 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {})
17337
17552
  // If the attribute-name case-insensitively matches the string
17338
17553
  // "SameSite", the user agent MUST process the cookie-av as follows:
17339
17554
 
17340
- // 1. Let enforcement be "Default".
17341
- let enforcement = 'Default'
17342
-
17343
17555
  const attributeValueLowercase = attributeValue.toLowerCase()
17344
- // 2. If cookie-av's attribute-value is a case-insensitive match for
17345
- // "None", set enforcement to "None".
17346
- if (attributeValueLowercase.includes('none')) {
17347
- enforcement = 'None'
17348
- }
17349
-
17350
- // 3. If cookie-av's attribute-value is a case-insensitive match for
17351
- // "Strict", set enforcement to "Strict".
17352
- if (attributeValueLowercase.includes('strict')) {
17353
- enforcement = 'Strict'
17354
- }
17355
17556
 
17356
- // 4. If cookie-av's attribute-value is a case-insensitive match for
17357
- // "Lax", set enforcement to "Lax".
17358
- if (attributeValueLowercase.includes('lax')) {
17359
- enforcement = 'Lax'
17557
+ // 1. If cookie-av's attribute-value is a case-insensitive match for
17558
+ // "None", append an attribute to the cookie-attribute-list with an
17559
+ // attribute-name of "SameSite" and an attribute-value of "None".
17560
+ if (attributeValueLowercase === 'none') {
17561
+ cookieAttributeList.sameSite = 'None'
17562
+ } else if (attributeValueLowercase === 'strict') {
17563
+ // 2. If cookie-av's attribute-value is a case-insensitive match for
17564
+ // "Strict", append an attribute to the cookie-attribute-list with
17565
+ // an attribute-name of "SameSite" and an attribute-value of
17566
+ // "Strict".
17567
+ cookieAttributeList.sameSite = 'Strict'
17568
+ } else if (attributeValueLowercase === 'lax') {
17569
+ // 3. If cookie-av's attribute-value is a case-insensitive match for
17570
+ // "Lax", append an attribute to the cookie-attribute-list with an
17571
+ // attribute-name of "SameSite" and an attribute-value of "Lax".
17572
+ cookieAttributeList.sameSite = 'Lax'
17360
17573
  }
17361
-
17362
- // 5. Append an attribute to the cookie-attribute-list with an
17363
- // attribute-name of "SameSite" and an attribute-value of
17364
- // enforcement.
17365
- cookieAttributeList.sameSite = enforcement
17366
17574
  } else {
17367
17575
  cookieAttributeList.unparsed ??= []
17368
17576
 
@@ -17492,7 +17700,7 @@ function validateCookiePath (path) {
17492
17700
 
17493
17701
  if (
17494
17702
  code < 0x20 || // exclude CTLs (0-31)
17495
- code === 0x7F || // DEL
17703
+ code > 0x7E || // exclude DEL and non-ascii
17496
17704
  code === 0x3B // ;
17497
17705
  ) {
17498
17706
  throw new Error('Invalid cookie path')
@@ -17501,16 +17709,80 @@ function validateCookiePath (path) {
17501
17709
  }
17502
17710
 
17503
17711
  /**
17504
- * I have no idea why these values aren't allowed to be honest,
17505
- * but Deno tests these. - Khafra
17712
+ * <let-dig> ::= <letter> | <digit>
17713
+ *
17714
+ * <letter> ::= any one of the 52 alphabetic characters A through Z in
17715
+ * upper case and a through z in lower case
17716
+ *
17717
+ * <digit> ::= any one of the ten digits 0 through 9r
17718
+ *
17719
+ * @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
17720
+ * @param {number} code
17721
+ */
17722
+ function isLetterOrDigit (code) {
17723
+ return (
17724
+ (code >= 0x30 && code <= 0x39) || // 0-9
17725
+ (code >= 0x41 && code <= 0x5A) || // A-Z
17726
+ (code >= 0x61 && code <= 0x7A) // a-z
17727
+ )
17728
+ }
17729
+
17730
+ /**
17731
+ * Validates a cookie domain against the "preferred name syntax".
17732
+ *
17733
+ * <domain> ::= <subdomain> | " "
17734
+ * <subdomain> ::= <label> | <subdomain> "." <label>
17735
+ * <label> ::= <let-dig> [ [ <ldh-str> ] <let-dig> ]
17736
+ * <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
17737
+ * <let-dig-hyp> ::= <let-dig> | "-"
17738
+ *
17739
+ * @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
17740
+ * @see https://www.rfc-editor.org/rfc/rfc1123#section-2.1
17741
+ * @see https://www.rfc-editor.org/rfc/rfc1035#section-2.3.4
17506
17742
  * @param {string} domain
17507
17743
  */
17508
17744
  function validateCookieDomain (domain) {
17509
- if (
17510
- domain.startsWith('-') ||
17511
- domain.endsWith('.') ||
17512
- domain.endsWith('-')
17513
- ) {
17745
+ // <domain> ::= <subdomain> | " "
17746
+ if (domain === ' ') {
17747
+ return
17748
+ }
17749
+
17750
+ if (domain.length > 255) {
17751
+ throw new Error('Invalid cookie domain')
17752
+ }
17753
+
17754
+ let labelLength = 0
17755
+
17756
+ for (let i = 0; i < domain.length; ++i) {
17757
+ const code = domain.charCodeAt(i)
17758
+
17759
+ if (code === 0x2E) {
17760
+ if (labelLength === 0) {
17761
+ throw new Error('Invalid cookie domain')
17762
+ }
17763
+
17764
+ if (domain.charCodeAt(i - 1) === 0x2D) { // "-"
17765
+ throw new Error('Invalid cookie domain')
17766
+ }
17767
+
17768
+ labelLength = 0
17769
+ continue
17770
+ }
17771
+
17772
+ if (labelLength === 0 && !isLetterOrDigit(code)) {
17773
+ throw new Error('Invalid cookie domain')
17774
+ }
17775
+
17776
+ if (!isLetterOrDigit(code) && code !== 0x2D) { // "-"
17777
+ throw new Error('Invalid cookie domain')
17778
+ }
17779
+
17780
+ if (++labelLength > 63) {
17781
+ throw new Error('Invalid cookie domain')
17782
+ }
17783
+ }
17784
+
17785
+ if (labelLength === 0 || domain.charCodeAt(domain.length - 1) === 0x2D) { // "-"
17514
17786
  throw new Error('Invalid cookie domain')
17515
17787
  }
17516
17788
  }
@@ -17653,7 +17925,13 @@ function stringify (cookie) {
17653
17925
 
17654
17926
  const [key, ...value] = part.split('=')
17655
17927
 
17656
- out.push(`${key.trim()}=${value.join('=')}`)
17928
+ const trimmedKey = key.trim()
17929
+ const joinedValue = value.join('=')
17930
+
17931
+ validateCookieName(trimmedKey)
17932
+ validateCookieValue(joinedValue)
17933
+
17934
+ out.push(`${trimmedKey}=${joinedValue}`)
17657
17935
  }
17658
17936
 
17659
17937
  return out.join('; ')
@@ -17700,6 +17978,49 @@ const COLON = 0x3A
17700
17978
  */
17701
17979
  const SPACE = 0x20
17702
17980
 
17981
+ const DATA = Buffer.from('data')
17982
+ const EVENT = Buffer.from('event')
17983
+ const ID = Buffer.from('id')
17984
+ const RETRY = Buffer.from('retry')
17985
+
17986
+ function isASCIINumberBytes (buffer, start) {
17987
+ if (start >= buffer.length) {
17988
+ return false
17989
+ }
17990
+
17991
+ for (let i = start; i < buffer.length; i++) {
17992
+ if (buffer[i] < 0x30 || buffer[i] > 0x39) {
17993
+ return false
17994
+ }
17995
+ }
17996
+
17997
+ return true
17998
+ }
17999
+
18000
+ function isValidLastEventIdBytes (buffer, start) {
18001
+ for (let i = start; i < buffer.length; i++) {
18002
+ if (buffer[i] === 0x00) {
18003
+ return false
18004
+ }
18005
+ }
18006
+
18007
+ return true
18008
+ }
18009
+
18010
+ function isFieldName (line, length, field) {
18011
+ if (length !== field.length) {
18012
+ return false
18013
+ }
18014
+
18015
+ for (let i = 0; i < length; i++) {
18016
+ if (line[i] !== field[i]) {
18017
+ return false
18018
+ }
18019
+ }
18020
+
18021
+ return true
18022
+ }
18023
+
17703
18024
  /**
17704
18025
  * @typedef {object} EventSourceStreamEvent
17705
18026
  * @type {object}
@@ -17740,11 +18061,14 @@ class EventSourceStream extends Transform {
17740
18061
  eventEndCheck = false
17741
18062
 
17742
18063
  /**
17743
- * @type {Buffer}
18064
+ * @type {Buffer[]}
17744
18065
  */
17745
- buffer = null
18066
+ chunks = []
17746
18067
 
18068
+ chunkIndex = 0
17747
18069
  pos = 0
18070
+ lineChunkIndex = 0
18071
+ linePos = 0
17748
18072
 
17749
18073
  event = {
17750
18074
  data: undefined,
@@ -17783,92 +18107,20 @@ class EventSourceStream extends Transform {
17783
18107
  return
17784
18108
  }
17785
18109
 
17786
- // Cache the chunk in the buffer, as the data might not be complete while
17787
- // processing it
17788
- // TODO: Investigate if there is a more performant way to handle
17789
- // incoming chunks
17790
- // see: https://github.com/nodejs/undici/issues/2630
17791
- if (this.buffer) {
17792
- this.buffer = Buffer.concat([this.buffer, chunk])
17793
- } else {
17794
- this.buffer = chunk
17795
- }
18110
+ this.chunks.push(chunk)
17796
18111
 
17797
18112
  // Strip leading byte-order-mark if we opened the stream and started
17798
18113
  // the processing of the incoming data
17799
18114
  if (this.checkBOM) {
17800
- switch (this.buffer.length) {
17801
- case 1:
17802
- // Check if the first byte is the same as the first byte of the BOM
17803
- if (this.buffer[0] === BOM[0]) {
17804
- // If it is, we need to wait for more data
17805
- callback()
17806
- return
17807
- }
17808
- // Set the checkBOM flag to false as we don't need to check for the
17809
- // BOM anymore
17810
- this.checkBOM = false
17811
-
17812
- // The buffer only contains one byte so we need to wait for more data
17813
- callback()
17814
- return
17815
- case 2:
17816
- // Check if the first two bytes are the same as the first two bytes
17817
- // of the BOM
17818
- if (
17819
- this.buffer[0] === BOM[0] &&
17820
- this.buffer[1] === BOM[1]
17821
- ) {
17822
- // If it is, we need to wait for more data, because the third byte
17823
- // is needed to determine if it is the BOM or not
17824
- callback()
17825
- return
17826
- }
17827
-
17828
- // Set the checkBOM flag to false as we don't need to check for the
17829
- // BOM anymore
17830
- this.checkBOM = false
17831
- break
17832
- case 3:
17833
- // Check if the first three bytes are the same as the first three
17834
- // bytes of the BOM
17835
- if (
17836
- this.buffer[0] === BOM[0] &&
17837
- this.buffer[1] === BOM[1] &&
17838
- this.buffer[2] === BOM[2]
17839
- ) {
17840
- // If it is, we can drop the buffered data, as it is only the BOM
17841
- this.buffer = Buffer.alloc(0)
17842
- // Set the checkBOM flag to false as we don't need to check for the
17843
- // BOM anymore
17844
- this.checkBOM = false
17845
-
17846
- // Await more data
17847
- callback()
17848
- return
17849
- }
17850
- // If it is not the BOM, we can start processing the data
17851
- this.checkBOM = false
17852
- break
17853
- default:
17854
- // The buffer is longer than 3 bytes, so we can drop the BOM if it is
17855
- // present
17856
- if (
17857
- this.buffer[0] === BOM[0] &&
17858
- this.buffer[1] === BOM[1] &&
17859
- this.buffer[2] === BOM[2]
17860
- ) {
17861
- // Remove the BOM from the buffer
17862
- this.buffer = this.buffer.subarray(3)
17863
- }
17864
-
17865
- // Set the checkBOM flag to false as we don't need to check for the
17866
- this.checkBOM = false
17867
- break
18115
+ if (this.handleBOM()) {
18116
+ callback()
18117
+ return
17868
18118
  }
17869
18119
  }
17870
18120
 
17871
- while (this.pos < this.buffer.length) {
18121
+ while (this.hasCurrentByte()) {
18122
+ const byte = this.currentByte()
18123
+
17872
18124
  // If the previous line ended with an end-of-line, we need to check
17873
18125
  // if the next character is also an end-of-line.
17874
18126
  if (this.eventEndCheck) {
@@ -17881,10 +18133,9 @@ class EventSourceStream extends Transform {
17881
18133
  if (this.crlfCheck) {
17882
18134
  // If the current character is a line feed, we can remove it
17883
18135
  // from the buffer and reset the crlfCheck flag
17884
- if (this.buffer[this.pos] === LF) {
17885
- this.buffer = this.buffer.subarray(this.pos + 1)
17886
- this.pos = 0
18136
+ if (byte === LF) {
17887
18137
  this.crlfCheck = false
18138
+ this.consumeCurrentByte()
17888
18139
 
17889
18140
  // It is possible that the line feed is not the end of the
17890
18141
  // event. We need to check if the next character is an
@@ -17900,19 +18151,17 @@ class EventSourceStream extends Transform {
17900
18151
  this.crlfCheck = false
17901
18152
  }
17902
18153
 
17903
- if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
18154
+ if (byte === LF || byte === CR) {
17904
18155
  // If the current character is a carriage return, we need to
17905
18156
  // set the crlfCheck flag to true, as we need to check if the
17906
18157
  // next character is a line feed so we can remove it from the
17907
18158
  // buffer
17908
- if (this.buffer[this.pos] === CR) {
18159
+ if (byte === CR) {
17909
18160
  this.crlfCheck = true
17910
18161
  }
17911
18162
 
17912
- this.buffer = this.buffer.subarray(this.pos + 1)
17913
- this.pos = 0
17914
- if (
17915
- this.event.data !== undefined || this.event.event || this.event.id || this.event.retry) {
18163
+ this.consumeCurrentByte()
18164
+ if (this.hasPendingEvent()) {
17916
18165
  this.processEvent(this.event)
17917
18166
  }
17918
18167
  this.clearEvent()
@@ -17926,22 +18175,18 @@ class EventSourceStream extends Transform {
17926
18175
 
17927
18176
  // If the current character is an end-of-line, we can process the
17928
18177
  // line
17929
- if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
18178
+ if (byte === LF || byte === CR) {
17930
18179
  // If the current character is a carriage return, we need to
17931
18180
  // set the crlfCheck flag to true, as we need to check if the
17932
18181
  // next character is a line feed
17933
- if (this.buffer[this.pos] === CR) {
18182
+ if (byte === CR) {
17934
18183
  this.crlfCheck = true
17935
18184
  }
17936
18185
 
17937
18186
  // In any case, we can process the line as we reached an
17938
18187
  // end-of-line character
17939
- this.parseLine(this.buffer.subarray(0, this.pos), this.event)
17940
-
17941
- // Remove the processed line from the buffer
17942
- this.buffer = this.buffer.subarray(this.pos + 1)
17943
- // Reset the position as we removed the processed line from the buffer
17944
- this.pos = 0
18188
+ this.parseLine(this.readLine(), this.event)
18189
+ this.consumeCurrentByte()
17945
18190
  // A line was processed and this could be the end of the event. We need
17946
18191
  // to check if the next line is empty to determine if the event is
17947
18192
  // finished.
@@ -17949,7 +18194,7 @@ class EventSourceStream extends Transform {
17949
18194
  continue
17950
18195
  }
17951
18196
 
17952
- this.pos++
18197
+ this.advanceCursor()
17953
18198
  }
17954
18199
 
17955
18200
  callback()
@@ -17974,64 +18219,53 @@ class EventSourceStream extends Transform {
17974
18219
  return
17975
18220
  }
17976
18221
 
17977
- let field = ''
17978
- let value = ''
18222
+ let fieldLength = line.length
18223
+ let valueStart = line.length
17979
18224
 
17980
18225
  // If the line contains a U+003A COLON character (:)
17981
18226
  if (colonPosition !== -1) {
17982
- // Collect the characters on the line before the first U+003A COLON
17983
- // character (:), and let field be that string.
17984
- // TODO: Investigate if there is a more performant way to extract the
17985
- // field
17986
- // see: https://github.com/nodejs/undici/issues/2630
17987
- field = line.subarray(0, colonPosition).toString('utf8')
18227
+ fieldLength = colonPosition
17988
18228
 
17989
18229
  // Collect the characters on the line after the first U+003A COLON
17990
18230
  // character (:), and let value be that string.
17991
18231
  // If value starts with a U+0020 SPACE character, remove it from value.
17992
- let valueStart = colonPosition + 1
18232
+ valueStart = colonPosition + 1
17993
18233
  if (line[valueStart] === SPACE) {
17994
18234
  ++valueStart
17995
18235
  }
17996
- // TODO: Investigate if there is a more performant way to extract the
17997
- // value
17998
- // see: https://github.com/nodejs/undici/issues/2630
17999
- value = line.subarray(valueStart).toString('utf8')
18236
+ }
18000
18237
 
18001
- // Otherwise, the string is not empty but does not contain a U+003A COLON
18002
- // character (:)
18003
- } else {
18004
- // Process the field using the steps described below, using the whole
18005
- // line as the field name, and the empty string as the field value.
18006
- field = line.toString('utf8')
18007
- value = ''
18008
- }
18009
-
18010
- // Modify the event with the field name and value. The value is also
18011
- // decoded as UTF-8
18012
- switch (field) {
18013
- case 'data':
18014
- if (event[field] === undefined) {
18015
- event[field] = value
18016
- } else {
18017
- event[field] += `\n${value}`
18018
- }
18019
- break
18020
- case 'retry':
18021
- if (isASCIINumber(value)) {
18022
- event[field] = value
18023
- }
18024
- break
18025
- case 'id':
18026
- if (isValidLastEventId(value)) {
18027
- event[field] = value
18028
- }
18029
- break
18030
- case 'event':
18031
- if (value.length > 0) {
18032
- event[field] = value
18033
- }
18034
- break
18238
+ if (isFieldName(line, fieldLength, DATA)) {
18239
+ const value = line.toString('utf8', valueStart)
18240
+
18241
+ if (event.data === undefined) {
18242
+ event.data = value
18243
+ } else {
18244
+ event.data += `\n${value}`
18245
+ }
18246
+ return
18247
+ }
18248
+
18249
+ if (isFieldName(line, fieldLength, RETRY)) {
18250
+ if (isASCIINumberBytes(line, valueStart)) {
18251
+ event.retry = line.toString('utf8', valueStart)
18252
+ }
18253
+ return
18254
+ }
18255
+
18256
+ if (isFieldName(line, fieldLength, ID)) {
18257
+ if (isValidLastEventIdBytes(line, valueStart)) {
18258
+ event.id = line.toString('utf8', valueStart)
18259
+ }
18260
+ return
18261
+ }
18262
+
18263
+ if (isFieldName(line, fieldLength, EVENT)) {
18264
+ const value = line.toString('utf8', valueStart)
18265
+
18266
+ if (value.length > 0) {
18267
+ event.event = value
18268
+ }
18035
18269
  }
18036
18270
  }
18037
18271
 
@@ -18061,12 +18295,151 @@ class EventSourceStream extends Transform {
18061
18295
  }
18062
18296
 
18063
18297
  clearEvent () {
18064
- this.event = {
18065
- data: undefined,
18066
- event: undefined,
18067
- id: undefined,
18068
- retry: undefined
18298
+ this.event.data = undefined
18299
+ this.event.event = undefined
18300
+ this.event.id = undefined
18301
+ this.event.retry = undefined
18302
+ }
18303
+
18304
+ hasPendingEvent () {
18305
+ return this.event.data !== undefined ||
18306
+ this.event.event !== undefined ||
18307
+ this.event.id !== undefined ||
18308
+ this.event.retry !== undefined
18309
+ }
18310
+
18311
+ hasCurrentByte () {
18312
+ return this.chunkIndex < this.chunks.length &&
18313
+ this.pos < this.chunks[this.chunkIndex].length
18314
+ }
18315
+
18316
+ currentByte () {
18317
+ return this.chunks[this.chunkIndex][this.pos]
18318
+ }
18319
+
18320
+ consumeCurrentByte () {
18321
+ this.advanceCursor()
18322
+ this.syncLineStartToCursor()
18323
+ }
18324
+
18325
+ advanceCursor () {
18326
+ this.pos++
18327
+
18328
+ while (this.chunkIndex < this.chunks.length && this.pos >= this.chunks[this.chunkIndex].length) {
18329
+ this.chunkIndex++
18330
+ this.pos = 0
18331
+ }
18332
+ }
18333
+
18334
+ syncLineStartToCursor () {
18335
+ this.lineChunkIndex = this.chunkIndex
18336
+ this.linePos = this.pos
18337
+ this.dropConsumedChunks()
18338
+ }
18339
+
18340
+ dropConsumedChunks () {
18341
+ while (this.lineChunkIndex > 0) {
18342
+ this.chunks.shift()
18343
+ this.lineChunkIndex--
18344
+ this.chunkIndex--
18345
+ }
18346
+
18347
+ if (this.chunkIndex === this.chunks.length) {
18348
+ this.chunks.length = 0
18349
+ this.chunkIndex = 0
18350
+ this.pos = 0
18351
+ this.lineChunkIndex = 0
18352
+ this.linePos = 0
18353
+ }
18354
+ }
18355
+
18356
+ readLine () {
18357
+ if (this.lineChunkIndex === this.chunkIndex) {
18358
+ return this.chunks[this.chunkIndex].subarray(this.linePos, this.pos)
18359
+ }
18360
+
18361
+ const chunks = []
18362
+ let length = 0
18363
+
18364
+ for (let i = this.lineChunkIndex; i <= this.chunkIndex; i++) {
18365
+ const chunk = this.chunks[i]
18366
+ const start = i === this.lineChunkIndex ? this.linePos : 0
18367
+ const end = i === this.chunkIndex ? this.pos : chunk.length
18368
+ const slice = chunk.subarray(start, end)
18369
+ length += slice.length
18370
+ chunks.push(slice)
18371
+ }
18372
+
18373
+ return Buffer.concat(chunks, length)
18374
+ }
18375
+
18376
+ peekBufferedByte (offset) {
18377
+ let chunkIndex = this.lineChunkIndex
18378
+ let pos = this.linePos
18379
+
18380
+ while (chunkIndex < this.chunks.length) {
18381
+ const chunk = this.chunks[chunkIndex]
18382
+ const remaining = chunk.length - pos
18383
+
18384
+ if (offset < remaining) {
18385
+ return chunk[pos + offset]
18386
+ }
18387
+
18388
+ offset -= remaining
18389
+ chunkIndex++
18390
+ pos = 0
18391
+ }
18392
+ }
18393
+
18394
+ discardLeadingBytes (count) {
18395
+ while (count > 0 && this.lineChunkIndex < this.chunks.length) {
18396
+ const chunk = this.chunks[this.lineChunkIndex]
18397
+ const remaining = chunk.length - this.linePos
18398
+
18399
+ if (count < remaining) {
18400
+ this.linePos += count
18401
+ count = 0
18402
+ } else {
18403
+ count -= remaining
18404
+ this.lineChunkIndex++
18405
+ this.linePos = 0
18406
+ }
18069
18407
  }
18408
+
18409
+ this.chunkIndex = this.lineChunkIndex
18410
+ this.pos = this.linePos
18411
+ this.dropConsumedChunks()
18412
+ }
18413
+
18414
+ handleBOM () {
18415
+ const first = this.peekBufferedByte(0)
18416
+ const second = this.peekBufferedByte(1)
18417
+ const third = this.peekBufferedByte(2)
18418
+
18419
+ if (second === undefined) {
18420
+ if (first === BOM[0]) {
18421
+ return true
18422
+ }
18423
+
18424
+ this.checkBOM = false
18425
+ return true
18426
+ }
18427
+
18428
+ if (third === undefined) {
18429
+ if (first === BOM[0] && second === BOM[1]) {
18430
+ return true
18431
+ }
18432
+
18433
+ this.checkBOM = false
18434
+ return false
18435
+ }
18436
+
18437
+ if (first === BOM[0] && second === BOM[1] && third === BOM[2]) {
18438
+ this.discardLeadingBytes(3)
18439
+ }
18440
+
18441
+ this.checkBOM = false
18442
+ return !this.hasCurrentByte()
18070
18443
  }
18071
18444
  }
18072
18445
 
@@ -29358,7 +29731,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
29358
29731
  // is specified, the server needs to include the same field and one of
29359
29732
  // the selected subprotocol values in its response for the connection to
29360
29733
  // be established.
29361
- if (!requestProtocols.includes(secProtocol)) {
29734
+ if (requestProtocols === null || !requestProtocols.includes(secProtocol)) {
29362
29735
  failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.')
29363
29736
  return
29364
29737
  }
@@ -30068,40 +30441,35 @@ const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
30068
30441
  const kBuffer = Symbol('kBuffer')
30069
30442
  const kLength = Symbol('kLength')
30070
30443
 
30071
- // Default maximum decompressed message size: 4 MB
30072
- const kDefaultMaxDecompressedSize = 4 * 1024 * 1024
30073
-
30074
30444
  class PerMessageDeflate {
30075
30445
  /** @type {import('node:zlib').InflateRaw} */
30076
30446
  #inflate
30077
30447
 
30078
30448
  #options = {}
30079
30449
 
30080
- /** @type {boolean} */
30081
- #aborted = false
30082
-
30083
- /** @type {Function|null} */
30084
- #currentCallback = null
30450
+ #maxPayloadSize = 0
30085
30451
 
30086
30452
  /**
30087
30453
  * @param {Map<string, string>} extensions
30088
30454
  */
30089
- constructor (extensions) {
30455
+ constructor (extensions, options) {
30090
30456
  this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
30091
30457
  this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
30458
+
30459
+ this.#maxPayloadSize = options.maxPayloadSize
30092
30460
  }
30093
30461
 
30462
+ /**
30463
+ * Decompress a compressed payload.
30464
+ * @param {Buffer} chunk Compressed data
30465
+ * @param {boolean} fin Final fragment flag
30466
+ * @param {Function} callback Callback function
30467
+ */
30094
30468
  decompress (chunk, fin, callback) {
30095
30469
  // An endpoint uses the following algorithm to decompress a message.
30096
30470
  // 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the
30097
30471
  // payload of the message.
30098
30472
  // 2. Decompress the resulting data using DEFLATE.
30099
-
30100
- if (this.#aborted) {
30101
- callback(new MessageSizeExceededError())
30102
- return
30103
- }
30104
-
30105
30473
  if (!this.#inflate) {
30106
30474
  let windowBits = Z_DEFAULT_WINDOWBITS
30107
30475
 
@@ -30124,23 +30492,17 @@ class PerMessageDeflate {
30124
30492
  this.#inflate[kLength] = 0
30125
30493
 
30126
30494
  this.#inflate.on('data', (data) => {
30127
- if (this.#aborted) {
30128
- return
30129
- }
30130
-
30131
30495
  this.#inflate[kLength] += data.length
30132
30496
 
30133
- if (this.#inflate[kLength] > kDefaultMaxDecompressedSize) {
30134
- this.#aborted = true
30497
+ if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) {
30498
+ callback(new MessageSizeExceededError())
30499
+ // The inflater may still hold buffered input that can emit a late
30500
+ // zlib error. Remove the data listener, then deterministically stop
30501
+ // the stream so a subsequent 'error' cannot fire without a listener
30502
+ // (which would terminate the process as an unhandled error event).
30135
30503
  this.#inflate.removeAllListeners()
30136
30504
  this.#inflate.destroy()
30137
30505
  this.#inflate = null
30138
-
30139
- if (this.#currentCallback) {
30140
- const cb = this.#currentCallback
30141
- this.#currentCallback = null
30142
- cb(new MessageSizeExceededError())
30143
- }
30144
30506
  return
30145
30507
  }
30146
30508
 
@@ -30153,14 +30515,13 @@ class PerMessageDeflate {
30153
30515
  })
30154
30516
  }
30155
30517
 
30156
- this.#currentCallback = callback
30157
30518
  this.#inflate.write(chunk)
30158
30519
  if (fin) {
30159
30520
  this.#inflate.write(tail)
30160
30521
  }
30161
30522
 
30162
30523
  this.#inflate.flush(() => {
30163
- if (this.#aborted || !this.#inflate) {
30524
+ if (!this.#inflate) {
30164
30525
  return
30165
30526
  }
30166
30527
 
@@ -30168,7 +30529,6 @@ class PerMessageDeflate {
30168
30529
 
30169
30530
  this.#inflate[kBuffer].length = 0
30170
30531
  this.#inflate[kLength] = 0
30171
- this.#currentCallback = null
30172
30532
 
30173
30533
  callback(null, full)
30174
30534
  })
@@ -30204,6 +30564,12 @@ const {
30204
30564
  const { WebsocketFrameSend } = __nccwpck_require__(9272)
30205
30565
  const { closeWebSocketConnection } = __nccwpck_require__(2569)
30206
30566
  const { PerMessageDeflate } = __nccwpck_require__(2869)
30567
+ const { MessageSizeExceededError } = __nccwpck_require__(8091)
30568
+
30569
+ function failWebsocketConnectionWithCode (ws, code, reason) {
30570
+ closeWebSocketConnection(ws, code, reason, Buffer.byteLength(reason))
30571
+ failWebsocketConnection(ws, reason)
30572
+ }
30207
30573
 
30208
30574
  // This code was influenced by ws released under the MIT license.
30209
30575
  // Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
@@ -30212,6 +30578,7 @@ const { PerMessageDeflate } = __nccwpck_require__(2869)
30212
30578
 
30213
30579
  class ByteParser extends Writable {
30214
30580
  #buffers = []
30581
+ #fragmentsBytes = 0
30215
30582
  #byteOffset = 0
30216
30583
  #loop = false
30217
30584
 
@@ -30223,18 +30590,27 @@ class ByteParser extends Writable {
30223
30590
  /** @type {Map<string, PerMessageDeflate>} */
30224
30591
  #extensions
30225
30592
 
30593
+ /** @type {number} */
30594
+ #maxFragments
30595
+
30596
+ /** @type {number} */
30597
+ #maxPayloadSize
30598
+
30226
30599
  /**
30227
30600
  * @param {import('./websocket').WebSocket} ws
30228
30601
  * @param {Map<string, string>|null} extensions
30602
+ * @param {{ maxFragments?: number, maxPayloadSize?: number }} [options]
30229
30603
  */
30230
- constructor (ws, extensions) {
30604
+ constructor (ws, extensions, options = {}) {
30231
30605
  super()
30232
30606
 
30233
30607
  this.ws = ws
30234
30608
  this.#extensions = extensions == null ? new Map() : extensions
30609
+ this.#maxFragments = options.maxFragments ?? 0
30610
+ this.#maxPayloadSize = options.maxPayloadSize ?? 0
30235
30611
 
30236
30612
  if (this.#extensions.has('permessage-deflate')) {
30237
- this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions))
30613
+ this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions, options))
30238
30614
  }
30239
30615
  }
30240
30616
 
@@ -30250,6 +30626,19 @@ class ByteParser extends Writable {
30250
30626
  this.run(callback)
30251
30627
  }
30252
30628
 
30629
+ #validatePayloadLength () {
30630
+ if (
30631
+ this.#maxPayloadSize > 0 &&
30632
+ !isControlFrame(this.#info.opcode) &&
30633
+ this.#info.payloadLength + this.#fragmentsBytes > this.#maxPayloadSize
30634
+ ) {
30635
+ failWebsocketConnectionWithCode(this.ws, 1009, 'Payload size exceeds maximum allowed size')
30636
+ return false
30637
+ }
30638
+
30639
+ return true
30640
+ }
30641
+
30253
30642
  /**
30254
30643
  * Runs whenever a new chunk is received.
30255
30644
  * Callback is called whenever there are no more chunks buffering,
@@ -30338,6 +30727,10 @@ class ByteParser extends Writable {
30338
30727
  if (payloadLength <= 125) {
30339
30728
  this.#info.payloadLength = payloadLength
30340
30729
  this.#state = parserStates.READ_DATA
30730
+
30731
+ if (!this.#validatePayloadLength()) {
30732
+ return
30733
+ }
30341
30734
  } else if (payloadLength === 126) {
30342
30735
  this.#state = parserStates.PAYLOADLENGTH_16
30343
30736
  } else if (payloadLength === 127) {
@@ -30362,6 +30755,10 @@ class ByteParser extends Writable {
30362
30755
 
30363
30756
  this.#info.payloadLength = buffer.readUInt16BE(0)
30364
30757
  this.#state = parserStates.READ_DATA
30758
+
30759
+ if (!this.#validatePayloadLength()) {
30760
+ return
30761
+ }
30365
30762
  } else if (this.#state === parserStates.PAYLOADLENGTH_64) {
30366
30763
  if (this.#byteOffset < 8) {
30367
30764
  return callback()
@@ -30384,6 +30781,10 @@ class ByteParser extends Writable {
30384
30781
 
30385
30782
  this.#info.payloadLength = lower
30386
30783
  this.#state = parserStates.READ_DATA
30784
+
30785
+ if (!this.#validatePayloadLength()) {
30786
+ return
30787
+ }
30387
30788
  } else if (this.#state === parserStates.READ_DATA) {
30388
30789
  if (this.#byteOffset < this.#info.payloadLength) {
30389
30790
  return callback()
@@ -30396,42 +30797,58 @@ class ByteParser extends Writable {
30396
30797
  this.#state = parserStates.INFO
30397
30798
  } else {
30398
30799
  if (!this.#info.compressed) {
30399
- this.#fragments.push(body)
30800
+ if (!this.writeFragments(body)) {
30801
+ return
30802
+ }
30803
+
30804
+ if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) {
30805
+ failWebsocketConnectionWithCode(this.ws, 1009, new MessageSizeExceededError().message)
30806
+ return
30807
+ }
30400
30808
 
30401
30809
  // If the frame is not fragmented, a message has been received.
30402
30810
  // If the frame is fragmented, it will terminate with a fin bit set
30403
30811
  // and an opcode of 0 (continuation), therefore we handle that when
30404
30812
  // parsing continuation frames, not here.
30405
30813
  if (!this.#info.fragmented && this.#info.fin) {
30406
- const fullMessage = Buffer.concat(this.#fragments)
30407
- websocketMessageReceived(this.ws, this.#info.binaryType, fullMessage)
30408
- this.#fragments.length = 0
30814
+ websocketMessageReceived(this.ws, this.#info.binaryType, this.consumeFragments())
30409
30815
  }
30410
30816
 
30411
30817
  this.#state = parserStates.INFO
30412
30818
  } else {
30413
- this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
30414
- if (error) {
30415
- failWebsocketConnection(this.ws, error.message)
30416
- return
30417
- }
30819
+ this.#extensions.get('permessage-deflate').decompress(
30820
+ body,
30821
+ this.#info.fin,
30822
+ (error, data) => {
30823
+ if (error) {
30824
+ const code = error instanceof MessageSizeExceededError ? 1009 : 1007
30825
+ failWebsocketConnectionWithCode(this.ws, code, error.message)
30826
+ return
30827
+ }
30418
30828
 
30419
- this.#fragments.push(data)
30829
+ if (!this.writeFragments(data)) {
30830
+ return
30831
+ }
30832
+
30833
+ if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) {
30834
+ failWebsocketConnectionWithCode(this.ws, 1009, new MessageSizeExceededError().message)
30835
+ return
30836
+ }
30837
+
30838
+ if (!this.#info.fin) {
30839
+ this.#state = parserStates.INFO
30840
+ this.#loop = true
30841
+ this.run(callback)
30842
+ return
30843
+ }
30844
+
30845
+ websocketMessageReceived(this.ws, this.#info.binaryType, this.consumeFragments())
30420
30846
 
30421
- if (!this.#info.fin) {
30422
- this.#state = parserStates.INFO
30423
30847
  this.#loop = true
30848
+ this.#state = parserStates.INFO
30424
30849
  this.run(callback)
30425
- return
30426
30850
  }
30427
-
30428
- websocketMessageReceived(this.ws, this.#info.binaryType, Buffer.concat(this.#fragments))
30429
-
30430
- this.#loop = true
30431
- this.#state = parserStates.INFO
30432
- this.#fragments.length = 0
30433
- this.run(callback)
30434
- })
30851
+ )
30435
30852
 
30436
30853
  this.#loop = false
30437
30854
  break
@@ -30483,6 +30900,35 @@ class ByteParser extends Writable {
30483
30900
  return buffer
30484
30901
  }
30485
30902
 
30903
+ writeFragments (fragment) {
30904
+ if (
30905
+ this.#maxFragments > 0 &&
30906
+ this.#fragments.length === this.#maxFragments
30907
+ ) {
30908
+ failWebsocketConnectionWithCode(this.ws, 1008, 'Too many message fragments')
30909
+ return false
30910
+ }
30911
+
30912
+ this.#fragmentsBytes += fragment.length
30913
+ this.#fragments.push(fragment)
30914
+ return true
30915
+ }
30916
+
30917
+ consumeFragments () {
30918
+ const fragments = this.#fragments
30919
+
30920
+ if (fragments.length === 1) {
30921
+ this.#fragmentsBytes = 0
30922
+ return fragments.shift()
30923
+ }
30924
+
30925
+ const output = Buffer.concat(fragments, this.#fragmentsBytes)
30926
+ this.#fragments = []
30927
+ this.#fragmentsBytes = 0
30928
+
30929
+ return output
30930
+ }
30931
+
30486
30932
  parseCloseBody (data) {
30487
30933
  assert(data.length !== 1)
30488
30934
 
@@ -31518,7 +31964,14 @@ class WebSocket extends EventTarget {
31518
31964
  // once this happens, the connection is open
31519
31965
  this[kResponse] = response
31520
31966
 
31521
- const parser = new ByteParser(this, parsedExtensions)
31967
+ const webSocketOptions = this[kController]?.dispatcher?.webSocketOptions
31968
+ const maxFragments = webSocketOptions?.maxFragments
31969
+ const maxPayloadSize = webSocketOptions?.maxPayloadSize
31970
+
31971
+ const parser = new ByteParser(this, parsedExtensions, {
31972
+ maxFragments,
31973
+ maxPayloadSize
31974
+ })
31522
31975
  parser.on('drain', onParserDrain)
31523
31976
  parser.on('error', onParserError.bind(this))
31524
31977
 
@@ -32606,7 +33059,7 @@ async function run() {
32606
33059
  // Parse test results
32607
33060
  const testResults = (0, parsers_1.parseTestOutput)(testOutput);
32608
33061
  if (!testResults) {
32609
- core.setFailed('Could not parse test output. Supported: Jest, Vitest.');
33062
+ core.setFailed(parsers_1.UNPARSEABLE_OUTPUT);
32610
33063
  return;
32611
33064
  }
32612
33065
  core.info(`Parsed results: ${testResults.passed}/${testResults.total} tests passing`);
@@ -32779,23 +33232,41 @@ function parseBunOutput(output) {
32779
33232
  * Tries each framework parser in order until one succeeds.
32780
33233
  */
32781
33234
  Object.defineProperty(exports, "__esModule", ({ value: true }));
33235
+ exports.UNPARSEABLE_OUTPUT = exports.SUPPORTED_PARSERS = exports.parseWjttcOutput = void 0;
32782
33236
  exports.parseTestOutput = parseTestOutput;
32783
33237
  const bun_1 = __nccwpck_require__(1505);
32784
33238
  const jest_1 = __nccwpck_require__(3416);
32785
33239
  const vitest_1 = __nccwpck_require__(9839);
33240
+ const wjttc_1 = __nccwpck_require__(8750);
33241
+ var wjttc_2 = __nccwpck_require__(8750);
33242
+ Object.defineProperty(exports, "parseWjttcOutput", ({ enumerable: true, get: function () { return wjttc_2.parseWjttcOutput; } }));
33243
+ /**
33244
+ * Every parser, in the order parseTestOutput tries them. Order matters:
33245
+ * 1. Bun — `Ran N tests across M files` is bun-unique.
33246
+ * 2. WJTTC — `pc-ai WJTTC` / suite `pc-ai-wjttc` (FAF bar; not Jest-shaped).
33247
+ * 3. Jest — `Tests: … total`.
33248
+ * 4. Vitest — `Tests … (N)`.
33249
+ */
33250
+ const PARSERS = [
33251
+ ['Bun', bun_1.parseBunOutput],
33252
+ ['WJTTC', wjttc_1.parseWjttcOutput],
33253
+ ['Jest', jest_1.parseJestOutput],
33254
+ ['Vitest', vitest_1.parseVitestOutput],
33255
+ ];
33256
+ exports.SUPPORTED_PARSERS = PARSERS.map(([name]) => name);
33257
+ /** What the CLI and the Action report when no parser matches. */
33258
+ exports.UNPARSEABLE_OUTPUT = `Could not parse test output. Supported: ${exports.SUPPORTED_PARSERS.join(', ')}.`;
32786
33259
  /**
32787
- * Parse test output from any supported framework.
32788
- *
32789
- * Order matters: Bun is tried first because its `Ran N tests across M files`
32790
- * anchor is bun-unique — it never false-matches on jest/vitest output. Putting
32791
- * it first also lets bun consumers ship truthful totals even when their CI
32792
- * still has a legacy `Tests: N passed, N total` jest-shim line appended to the
32793
- * test output (the bun parser ignores it and reads the canonical `Ran` line).
32794
- *
32795
- * Tries parsers in order: Bun, Jest, Vitest.
33260
+ * Parse test output from any supported framework: the first parser in
33261
+ * PARSERS that recognises the output wins.
32796
33262
  */
32797
33263
  function parseTestOutput(output) {
32798
- return (0, bun_1.parseBunOutput)(output) || (0, jest_1.parseJestOutput)(output) || (0, vitest_1.parseVitestOutput)(output);
33264
+ for (const [, parse] of PARSERS) {
33265
+ const results = parse(output);
33266
+ if (results)
33267
+ return results;
33268
+ }
33269
+ return null;
32799
33270
  }
32800
33271
 
32801
33272
 
@@ -32961,6 +33432,165 @@ function parseVitestOutput(output) {
32961
33432
  }
32962
33433
 
32963
33434
 
33435
+ /***/ }),
33436
+
33437
+ /***/ 8750:
33438
+ /***/ ((__unused_webpack_module, exports) => {
33439
+
33440
+ "use strict";
33441
+
33442
+ /**
33443
+ * WJTTC output parser
33444
+ *
33445
+ * Extracts test counts from the FAF / pc-ai WJTTC bar runner
33446
+ * (tests/wjttc/run-wjttc.mjs and sibling suite runners).
33447
+ *
33448
+ * Two shapes:
33449
+ *
33450
+ * 1) Human log (default):
33451
+ * ```
33452
+ * pc-ai WJTTC
33453
+ * ----------
33454
+ * PASS VAL-01 valid-simple portfolio accepts
33455
+ * FAIL OPT-01 …
33456
+ * ----------
33457
+ * 31/33 passed
33458
+ * ```
33459
+ *
33460
+ * 2) Machine JSON (`--json`):
33461
+ * ```
33462
+ * {
33463
+ * "suite": "pc-ai-wjttc",
33464
+ * "total": 33,
33465
+ * "passed": 31,
33466
+ * "failed": 2,
33467
+ * "results": [ … ]
33468
+ * }
33469
+ * ```
33470
+ *
33471
+ * Anchors are WJTTC-specific so this never false-matches Jest/Vitest/Bun.
33472
+ * Prefer JSON when both appear in a teed stream (structured totals win).
33473
+ */
33474
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
33475
+ exports.parseWjttcOutput = parseWjttcOutput;
33476
+ function stripAnsi(output) {
33477
+ // eslint-disable-next-line no-control-regex
33478
+ return output
33479
+ .replace(/\x1b\[[0-9;]*m/g, '')
33480
+ .replace(/\r\n/g, '\n')
33481
+ .replace(/\r/g, '\n');
33482
+ }
33483
+ /**
33484
+ * Try to parse a WJTTC --json summary object from output.
33485
+ * Handles pure JSON or JSON embedded after build noise in a tee file.
33486
+ */
33487
+ function parseJsonSuite(clean) {
33488
+ // Prefer a top-level object that names the suite.
33489
+ const candidates = [];
33490
+ const trimmed = clean.trim();
33491
+ if (trimmed.startsWith('{')) {
33492
+ candidates.push(trimmed);
33493
+ }
33494
+ // Last {...} block often is the suite when logs precede JSON.
33495
+ const lastBrace = clean.lastIndexOf('{');
33496
+ if (lastBrace !== -1) {
33497
+ candidates.push(clean.slice(lastBrace));
33498
+ }
33499
+ for (const raw of candidates) {
33500
+ try {
33501
+ const body = JSON.parse(raw);
33502
+ if (!body || typeof body !== 'object')
33503
+ continue;
33504
+ if (body.suite !== 'pc-ai-wjttc')
33505
+ continue;
33506
+ const total = Number(body.total);
33507
+ const passed = Number(body.passed);
33508
+ const failed = Number(body.failed);
33509
+ if (!Number.isFinite(total) || total <= 0)
33510
+ continue;
33511
+ if (!Number.isFinite(passed) || !Number.isFinite(failed))
33512
+ continue;
33513
+ // Prefer explicit fields; fall back to results[] length if needed.
33514
+ let p = passed;
33515
+ let f = failed;
33516
+ let t = total;
33517
+ if (Array.isArray(body.results) && body.results.length > 0) {
33518
+ const fromRows = body.results.length;
33519
+ if (t !== fromRows && fromRows > 0) {
33520
+ // Trust results array as the executed set (partition honesty).
33521
+ t = fromRows;
33522
+ p = body.results.filter((r) => r && r.ok === true).length;
33523
+ f = t - p;
33524
+ }
33525
+ }
33526
+ return {
33527
+ total: t,
33528
+ passed: p,
33529
+ failed: f,
33530
+ result: f > 0 ? 'FAILED' : 'PASSED',
33531
+ };
33532
+ }
33533
+ catch {
33534
+ // try next candidate
33535
+ }
33536
+ }
33537
+ return null;
33538
+ }
33539
+ /**
33540
+ * Human WJTTC log: header + optional PASS/FAIL lines + "N/N passed".
33541
+ */
33542
+ function parseHumanLog(clean) {
33543
+ // Require the suite banner so we do not steal "12/12 passed" from other tools.
33544
+ if (!/pc-ai\s+WJTTC/i.test(clean)) {
33545
+ return null;
33546
+ }
33547
+ const summary = clean.match(/(\d+)\s*\/\s*(\d+)\s+passed\b/i);
33548
+ if (!summary)
33549
+ return null;
33550
+ const passedFromSummary = parseInt(summary[1], 10);
33551
+ const totalFromSummary = parseInt(summary[2], 10);
33552
+ if (totalFromSummary <= 0)
33553
+ return null;
33554
+ // Cross-check with PASS/FAIL lines when present (stronger than summary alone).
33555
+ const passLines = clean.match(/^PASS\s+\S+/gm);
33556
+ const failLines = clean.match(/^FAIL\s+\S+/gm);
33557
+ const linePassed = passLines ? passLines.length : -1;
33558
+ const lineFailed = failLines ? failLines.length : -1;
33559
+ let total = totalFromSummary;
33560
+ let passed = passedFromSummary;
33561
+ let failed = total - passed;
33562
+ if (linePassed >= 0 && lineFailed >= 0) {
33563
+ const lineTotal = linePassed + lineFailed;
33564
+ if (lineTotal === totalFromSummary) {
33565
+ passed = linePassed;
33566
+ failed = lineFailed;
33567
+ total = lineTotal;
33568
+ }
33569
+ // If lines disagree with summary, still use summary (runner is source of truth)
33570
+ // but failed must be non-negative.
33571
+ if (failed < 0)
33572
+ failed = 0;
33573
+ }
33574
+ return {
33575
+ total,
33576
+ passed,
33577
+ failed,
33578
+ result: failed > 0 ? 'FAILED' : 'PASSED',
33579
+ };
33580
+ }
33581
+ /**
33582
+ * Parse WJTTC (pc-ai bar) output to TestResults, or null if not WJTTC.
33583
+ */
33584
+ function parseWjttcOutput(output) {
33585
+ const clean = stripAnsi(output);
33586
+ // JSON first — structured totals when --json was used.
33587
+ const fromJson = parseJsonSuite(clean);
33588
+ if (fromJson)
33589
+ return fromJson;
33590
+ return parseHumanLog(clean);
33591
+ }
33592
+
33593
+
32964
33594
  /***/ }),
32965
33595
 
32966
33596
  /***/ 390:
@@ -33461,7 +34091,7 @@ function composeCollection(CN, ctx, token, props, onError) {
33461
34091
  let tag = ctx.schema.tags.find(t => t.tag === tagName && t.collection === expType);
33462
34092
  if (!tag) {
33463
34093
  const kt = ctx.schema.knownTags[tagName];
33464
- if (kt && kt.collection === expType) {
34094
+ if (kt?.collection === expType) {
33465
34095
  ctx.schema.tags.push(Object.assign({}, kt, { default: false }));
33466
34096
  tag = kt;
33467
34097
  }
@@ -33581,19 +34211,26 @@ function composeNode(ctx, token, props, onError) {
33581
34211
  case 'block-map':
33582
34212
  case 'block-seq':
33583
34213
  case 'flow-collection':
33584
- node = composeCollection.composeCollection(CN, ctx, token, props, onError);
33585
- if (anchor)
33586
- node.anchor = anchor.source.substring(1);
34214
+ try {
34215
+ node = composeCollection.composeCollection(CN, ctx, token, props, onError);
34216
+ if (anchor)
34217
+ node.anchor = anchor.source.substring(1);
34218
+ }
34219
+ catch (error) {
34220
+ // Almost certainly here due to a stack overflow
34221
+ const message = error instanceof Error ? error.message : String(error);
34222
+ onError(token, 'RESOURCE_EXHAUSTION', message);
34223
+ }
33587
34224
  break;
33588
34225
  default: {
33589
34226
  const message = token.type === 'error'
33590
34227
  ? token.message
33591
34228
  : `Unsupported token (type: ${token.type})`;
33592
34229
  onError(token, 'UNEXPECTED_TOKEN', message);
33593
- node = composeEmptyNode(ctx, token.offset, undefined, null, props, onError);
33594
34230
  isSrcToken = false;
33595
34231
  }
33596
34232
  }
34233
+ node ?? (node = composeEmptyNode(ctx, token.offset, undefined, null, props, onError));
33597
34234
  if (anchor && node.anchor === '')
33598
34235
  onError(anchor, 'BAD_ALIAS', 'Anchor cannot be an empty string');
33599
34236
  if (atKey &&
@@ -33857,8 +34494,10 @@ class Composer {
33857
34494
  }
33858
34495
  }
33859
34496
  if (afterDoc) {
33860
- Array.prototype.push.apply(doc.errors, this.errors);
33861
- Array.prototype.push.apply(doc.warnings, this.warnings);
34497
+ for (let i = 0; i < this.errors.length; ++i)
34498
+ doc.errors.push(this.errors[i]);
34499
+ for (let i = 0; i < this.warnings.length; ++i)
34500
+ doc.warnings.push(this.warnings[i]);
33862
34501
  }
33863
34502
  else {
33864
34503
  doc.errors = this.errors;
@@ -34347,7 +34986,7 @@ function resolveBlockSeq({ composeNode, composeEmptyNode }, ctx, bs, onError, ta
34347
34986
  });
34348
34987
  if (!props.found) {
34349
34988
  if (props.anchor || props.tag || value) {
34350
- if (value && value.type === 'block-seq')
34989
+ if (value?.type === 'block-seq')
34351
34990
  onError(props.end, 'BAD_INDENT', 'All sequence items must start at the same column');
34352
34991
  else
34353
34992
  onError(offset, 'MISSING_CHAR', 'Sequence item without - indicator');
@@ -34564,7 +35203,7 @@ function resolveFlowCollection({ composeNode, composeEmptyNode }, ctx, fc, onErr
34564
35203
  }
34565
35204
  }
34566
35205
  else if (value) {
34567
- if ('source' in value && value.source && value.source[0] === ':')
35206
+ if ('source' in value && value.source?.[0] === ':')
34568
35207
  onError(value, 'MISSING_CHAR', `Missing space after : in ${fcName}`);
34569
35208
  else
34570
35209
  onError(valueProps.start, 'MISSING_CHAR', `Missing , or : between ${fcName} items`);
@@ -34608,7 +35247,7 @@ function resolveFlowCollection({ composeNode, composeEmptyNode }, ctx, fc, onErr
34608
35247
  const expectedEnd = isMap ? '}' : ']';
34609
35248
  const [ce, ...ee] = fc.end;
34610
35249
  let cePos = offset;
34611
- if (ce && ce.source === expectedEnd)
35250
+ if (ce?.source === expectedEnd)
34612
35251
  cePos = ce.offset + ce.source.length;
34613
35252
  else {
34614
35253
  const name = fcName[0].toUpperCase() + fcName.substring(1);
@@ -34790,7 +35429,7 @@ function doubleQuotedValue(source, onError) {
34790
35429
  next = source[++i + 1];
34791
35430
  }
34792
35431
  else if (next === 'x' || next === 'u' || next === 'U') {
34793
- const length = { x: 2, u: 4, U: 8 }[next];
35432
+ const length = next === 'x' ? 2 : next === 'u' ? 4 : 8;
34794
35433
  res += parseCharCode(source, i + 1, length, onError);
34795
35434
  i += length;
34796
35435
  }
@@ -34860,12 +35499,14 @@ function parseCharCode(source, offset, length, onError) {
34860
35499
  const cc = source.substr(offset, length);
34861
35500
  const ok = cc.length === length && /^[0-9a-fA-F]+$/.test(cc);
34862
35501
  const code = ok ? parseInt(cc, 16) : NaN;
34863
- if (isNaN(code)) {
35502
+ try {
35503
+ return String.fromCodePoint(code);
35504
+ }
35505
+ catch {
34864
35506
  const raw = source.substr(offset - 2, length + 2);
34865
35507
  onError(offset - 2, 'BAD_DQ_ESCAPE', `Invalid escape sequence ${raw}`);
34866
35508
  return raw;
34867
35509
  }
34868
- return String.fromCodePoint(code);
34869
35510
  }
34870
35511
 
34871
35512
  exports.resolveFlowScalar = resolveFlowScalar;
@@ -35989,7 +36630,7 @@ const prettifyError = (src, lc) => (error) => {
35989
36630
  if (/[^ ]/.test(lineStr)) {
35990
36631
  let count = 1;
35991
36632
  const end = error.linePos[1];
35992
- if (end && end.line === line && end.col > col) {
36633
+ if (end?.line === line && end.col > col) {
35993
36634
  count = Math.max(1, Math.min(end.col - col, 80 - ci));
35994
36635
  }
35995
36636
  const pointer = ' '.repeat(ci) + '^'.repeat(count);
@@ -36117,6 +36758,8 @@ class Alias extends Node.NodeBase {
36117
36758
  * instance of the `source` anchor before this node.
36118
36759
  */
36119
36760
  resolve(doc, ctx) {
36761
+ if (ctx?.maxAliasCount === 0)
36762
+ throw new ReferenceError('Alias resolution is disabled');
36120
36763
  let nodes;
36121
36764
  if (ctx?.aliasResolveCache) {
36122
36765
  nodes = ctx.aliasResolveCache;
@@ -36157,7 +36800,7 @@ class Alias extends Node.NodeBase {
36157
36800
  data = anchors.get(source);
36158
36801
  }
36159
36802
  /* istanbul ignore if */
36160
- if (!data || data.res === undefined) {
36803
+ if (data?.res === undefined) {
36161
36804
  const msg = 'This should not happen: Alias anchor was not resolved?';
36162
36805
  throw new ReferenceError(msg);
36163
36806
  }
@@ -37804,7 +38447,7 @@ class Lexer {
37804
38447
  const n = (yield* this.pushCount(1)) + (yield* this.pushSpaces(true));
37805
38448
  this.indentNext = this.indentValue + 1;
37806
38449
  this.indentValue += n;
37807
- return yield* this.parseBlockStart();
38450
+ return 'block-start';
37808
38451
  }
37809
38452
  return 'doc';
37810
38453
  }
@@ -38125,32 +38768,36 @@ class Lexer {
38125
38768
  return 0;
38126
38769
  }
38127
38770
  *pushIndicators() {
38128
- switch (this.charAt(0)) {
38129
- case '!':
38130
- return ((yield* this.pushTag()) +
38131
- (yield* this.pushSpaces(true)) +
38132
- (yield* this.pushIndicators()));
38133
- case '&':
38134
- return ((yield* this.pushUntil(isNotAnchorChar)) +
38135
- (yield* this.pushSpaces(true)) +
38136
- (yield* this.pushIndicators()));
38137
- case '-': // this is an error
38138
- case '?': // this is an error outside flow collections
38139
- case ':': {
38140
- const inFlow = this.flowLevel > 0;
38141
- const ch1 = this.charAt(1);
38142
- if (isEmpty(ch1) || (inFlow && flowIndicatorChars.has(ch1))) {
38143
- if (!inFlow)
38144
- this.indentNext = this.indentValue + 1;
38145
- else if (this.flowKey)
38146
- this.flowKey = false;
38147
- return ((yield* this.pushCount(1)) +
38148
- (yield* this.pushSpaces(true)) +
38149
- (yield* this.pushIndicators()));
38771
+ let n = 0;
38772
+ loop: while (true) {
38773
+ switch (this.charAt(0)) {
38774
+ case '!':
38775
+ n += yield* this.pushTag();
38776
+ n += yield* this.pushSpaces(true);
38777
+ continue loop;
38778
+ case '&':
38779
+ n += yield* this.pushUntil(isNotAnchorChar);
38780
+ n += yield* this.pushSpaces(true);
38781
+ continue loop;
38782
+ case '-': // this is an error
38783
+ case '?': // this is an error outside flow collections
38784
+ case ':': {
38785
+ const inFlow = this.flowLevel > 0;
38786
+ const ch1 = this.charAt(1);
38787
+ if (isEmpty(ch1) || (inFlow && flowIndicatorChars.has(ch1))) {
38788
+ if (!inFlow)
38789
+ this.indentNext = this.indentValue + 1;
38790
+ else if (this.flowKey)
38791
+ this.flowKey = false;
38792
+ n += yield* this.pushCount(1);
38793
+ n += yield* this.pushSpaces(true);
38794
+ continue loop;
38795
+ }
38150
38796
  }
38151
38797
  }
38798
+ break loop;
38152
38799
  }
38153
- return 0;
38800
+ return n;
38154
38801
  }
38155
38802
  *pushTag() {
38156
38803
  if (this.charAt(1) === '<') {
@@ -38338,6 +38985,14 @@ function getFirstKeyStartProps(prev) {
38338
38985
  }
38339
38986
  return prev.splice(i, prev.length);
38340
38987
  }
38988
+ function arrayPushArray(target, source) {
38989
+ // May exhaust call stack with large `source` array
38990
+ if (source.length < 1e5)
38991
+ Array.prototype.push.apply(target, source);
38992
+ else
38993
+ for (let i = 0; i < source.length; ++i)
38994
+ target.push(source[i]);
38995
+ }
38341
38996
  function fixFlowSeqItems(fc) {
38342
38997
  if (fc.start.type === 'flow-seq-start') {
38343
38998
  for (const it of fc.items) {
@@ -38350,12 +39005,12 @@ function fixFlowSeqItems(fc) {
38350
39005
  delete it.key;
38351
39006
  if (isFlowToken(it.value)) {
38352
39007
  if (it.value.end)
38353
- Array.prototype.push.apply(it.value.end, it.sep);
39008
+ arrayPushArray(it.value.end, it.sep);
38354
39009
  else
38355
39010
  it.value.end = it.sep;
38356
39011
  }
38357
39012
  else
38358
- Array.prototype.push.apply(it.start, it.sep);
39013
+ arrayPushArray(it.start, it.sep);
38359
39014
  delete it.sep;
38360
39015
  }
38361
39016
  }
@@ -38499,7 +39154,7 @@ class Parser {
38499
39154
  }
38500
39155
  *step() {
38501
39156
  const top = this.peek(1);
38502
- if (this.type === 'doc-end' && (!top || top.type !== 'doc-end')) {
39157
+ if (this.type === 'doc-end' && top?.type !== 'doc-end') {
38503
39158
  while (this.stack.length > 0)
38504
39159
  yield* this.pop();
38505
39160
  this.stack.push({
@@ -38775,7 +39430,7 @@ class Parser {
38775
39430
  const prev = map.items[map.items.length - 2];
38776
39431
  const end = prev?.value?.end;
38777
39432
  if (Array.isArray(end)) {
38778
- Array.prototype.push.apply(end, it.start);
39433
+ arrayPushArray(end, it.start);
38779
39434
  end.push(this.sourceToken);
38780
39435
  map.items.pop();
38781
39436
  return;
@@ -38990,7 +39645,7 @@ class Parser {
38990
39645
  const prev = seq.items[seq.items.length - 2];
38991
39646
  const end = prev?.value?.end;
38992
39647
  if (Array.isArray(end)) {
38993
- Array.prototype.push.apply(end, it.start);
39648
+ arrayPushArray(end, it.start);
38994
39649
  end.push(this.sourceToken);
38995
39650
  seq.items.pop();
38996
39651
  return;
@@ -39031,7 +39686,7 @@ class Parser {
39031
39686
  do {
39032
39687
  yield* this.pop();
39033
39688
  top = this.peek(1);
39034
- } while (top && top.type === 'flow-collection');
39689
+ } while (top?.type === 'flow-collection');
39035
39690
  }
39036
39691
  else if (fc.end.length === 0) {
39037
39692
  switch (this.type) {
@@ -40144,18 +40799,18 @@ const isMergeKey = (ctx, key) => (merge.identify(key) ||
40144
40799
  merge.identify(key.value))) &&
40145
40800
  ctx?.doc.schema.tags.some(tag => tag.tag === merge.tag && tag.default);
40146
40801
  function addMergeToJSMap(ctx, map, value) {
40147
- value = ctx && identity.isAlias(value) ? value.resolve(ctx.doc) : value;
40148
- if (identity.isSeq(value))
40149
- for (const it of value.items)
40802
+ const source = resolveAliasValue(ctx, value);
40803
+ if (identity.isSeq(source))
40804
+ for (const it of source.items)
40150
40805
  mergeValue(ctx, map, it);
40151
- else if (Array.isArray(value))
40152
- for (const it of value)
40806
+ else if (Array.isArray(source))
40807
+ for (const it of source)
40153
40808
  mergeValue(ctx, map, it);
40154
40809
  else
40155
- mergeValue(ctx, map, value);
40810
+ mergeValue(ctx, map, source);
40156
40811
  }
40157
40812
  function mergeValue(ctx, map, value) {
40158
- const source = ctx && identity.isAlias(value) ? value.resolve(ctx.doc) : value;
40813
+ const source = resolveAliasValue(ctx, value);
40159
40814
  if (!identity.isMap(source))
40160
40815
  throw new Error('Merge sources must be maps or map aliases');
40161
40816
  const srcMap = source.toJSON(null, ctx, Map);
@@ -40178,6 +40833,9 @@ function mergeValue(ctx, map, value) {
40178
40833
  }
40179
40834
  return map;
40180
40835
  }
40836
+ function resolveAliasValue(ctx, value) {
40837
+ return ctx && identity.isAlias(value) ? value.resolve(ctx.doc, ctx) : value;
40838
+ }
40181
40839
 
40182
40840
  exports.addMergeToJSMap = addMergeToJSMap;
40183
40841
  exports.isMergeKey = isMergeKey;
@@ -40814,6 +41472,7 @@ function createStringifyContext(doc, options) {
40814
41472
  nullStr: 'null',
40815
41473
  simpleKeys: false,
40816
41474
  singleQuote: null,
41475
+ trailingComma: false,
40817
41476
  trueStr: 'true',
40818
41477
  verifyAliasOrder: true
40819
41478
  }, doc.schema.toStringOptions, options);
@@ -41035,12 +41694,22 @@ function stringifyFlowCollection({ items }, ctx, { flowChars, itemIndent }) {
41035
41694
  if (comment)
41036
41695
  reqNewline = true;
41037
41696
  let str = stringify.stringify(item, itemCtx, () => (comment = null));
41038
- if (i < items.length - 1)
41697
+ reqNewline || (reqNewline = lines.length > linesAtValue || str.includes('\n'));
41698
+ if (i < items.length - 1) {
41039
41699
  str += ',';
41700
+ }
41701
+ else if (ctx.options.trailingComma) {
41702
+ if (ctx.options.lineWidth > 0) {
41703
+ reqNewline || (reqNewline = lines.reduce((sum, line) => sum + line.length + 2, 2) +
41704
+ (str.length + 2) >
41705
+ ctx.options.lineWidth);
41706
+ }
41707
+ if (reqNewline) {
41708
+ str += ',';
41709
+ }
41710
+ }
41040
41711
  if (comment)
41041
41712
  str += stringifyComment.lineComment(str, itemIndent, commentString(comment));
41042
- if (!reqNewline && (lines.length > linesAtValue || str.includes('\n')))
41043
- reqNewline = true;
41044
41713
  lines.push(str);
41045
41714
  linesAtValue = lines.length;
41046
41715
  }
@@ -41217,11 +41886,12 @@ function stringifyNumber({ format, minFractionDigits, tag, value }) {
41217
41886
  const num = typeof value === 'number' ? value : Number(value);
41218
41887
  if (!isFinite(num))
41219
41888
  return isNaN(num) ? '.nan' : num < 0 ? '-.inf' : '.inf';
41220
- let n = JSON.stringify(value);
41889
+ let n = Object.is(value, -0) ? '-0' : JSON.stringify(value);
41221
41890
  if (!format &&
41222
41891
  minFractionDigits &&
41223
41892
  (!tag || tag === 'tag:yaml.org,2002:float') &&
41224
- /^\d/.test(n)) {
41893
+ /^-?\d/.test(n) &&
41894
+ !n.includes('e')) {
41225
41895
  let i = n.indexOf('.');
41226
41896
  if (i < 0) {
41227
41897
  i = n.length;
@@ -41348,7 +42018,7 @@ function stringifyPair({ key, value }, ctx, onComment, onChompKeep) {
41348
42018
  ws += `\n${stringifyComment.indentComment(cs, ctx.indent)}`;
41349
42019
  }
41350
42020
  if (valueStr === '' && !ctx.inFlow) {
41351
- if (ws === '\n')
42021
+ if (ws === '\n' && valueComment)
41352
42022
  ws = '\n\n';
41353
42023
  }
41354
42024
  else {