@suveren/gateway 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/README.md +33 -1
  2. package/bin/suveren-gateway.js +203 -5
  3. package/content/integrations/erp.json +184 -0
  4. package/content/integrations/index.json +2 -1
  5. package/dist/control-plane/index.mjs +653 -179
  6. package/dist/mcp-server/http.mjs +466 -259
  7. package/dist/ui/assets/{index-CIiOCgjJ.js → index-B1qNl7zo.js} +1 -1
  8. package/dist/ui/index.html +1 -1
  9. package/lib/as-pairing.mjs +77 -0
  10. package/lib/config.mjs +48 -0
  11. package/node_modules/undici/LICENSE +21 -0
  12. package/node_modules/undici/README.md +741 -0
  13. package/node_modules/undici/docs/docs/api/Agent.md +84 -0
  14. package/node_modules/undici/docs/docs/api/BalancedPool.md +99 -0
  15. package/node_modules/undici/docs/docs/api/CacheStorage.md +30 -0
  16. package/node_modules/undici/docs/docs/api/CacheStore.md +164 -0
  17. package/node_modules/undici/docs/docs/api/Client.md +288 -0
  18. package/node_modules/undici/docs/docs/api/ClientStats.md +27 -0
  19. package/node_modules/undici/docs/docs/api/Connector.md +115 -0
  20. package/node_modules/undici/docs/docs/api/ContentType.md +57 -0
  21. package/node_modules/undici/docs/docs/api/Cookies.md +128 -0
  22. package/node_modules/undici/docs/docs/api/Debug.md +62 -0
  23. package/node_modules/undici/docs/docs/api/DiagnosticsChannel.md +317 -0
  24. package/node_modules/undici/docs/docs/api/Dispatcher.md +1393 -0
  25. package/node_modules/undici/docs/docs/api/EnvHttpProxyAgent.md +159 -0
  26. package/node_modules/undici/docs/docs/api/Errors.md +49 -0
  27. package/node_modules/undici/docs/docs/api/EventSource.md +45 -0
  28. package/node_modules/undici/docs/docs/api/Fetch.md +60 -0
  29. package/node_modules/undici/docs/docs/api/GlobalInstallation.md +139 -0
  30. package/node_modules/undici/docs/docs/api/H2CClient.md +263 -0
  31. package/node_modules/undici/docs/docs/api/MockAgent.md +603 -0
  32. package/node_modules/undici/docs/docs/api/MockCallHistory.md +197 -0
  33. package/node_modules/undici/docs/docs/api/MockCallHistoryLog.md +43 -0
  34. package/node_modules/undici/docs/docs/api/MockClient.md +81 -0
  35. package/node_modules/undici/docs/docs/api/MockErrors.md +12 -0
  36. package/node_modules/undici/docs/docs/api/MockPool.md +555 -0
  37. package/node_modules/undici/docs/docs/api/Pool.md +84 -0
  38. package/node_modules/undici/docs/docs/api/PoolStats.md +35 -0
  39. package/node_modules/undici/docs/docs/api/ProxyAgent.md +229 -0
  40. package/node_modules/undici/docs/docs/api/RedirectHandler.md +93 -0
  41. package/node_modules/undici/docs/docs/api/RetryAgent.md +50 -0
  42. package/node_modules/undici/docs/docs/api/RetryHandler.md +118 -0
  43. package/node_modules/undici/docs/docs/api/RoundRobinPool.md +145 -0
  44. package/node_modules/undici/docs/docs/api/SnapshotAgent.md +616 -0
  45. package/node_modules/undici/docs/docs/api/Socks5ProxyAgent.md +275 -0
  46. package/node_modules/undici/docs/docs/api/Util.md +25 -0
  47. package/node_modules/undici/docs/docs/api/WebSocket.md +141 -0
  48. package/node_modules/undici/docs/docs/api/api-lifecycle.md +91 -0
  49. package/node_modules/undici/docs/docs/best-practices/client-certificate.md +64 -0
  50. package/node_modules/undici/docs/docs/best-practices/crawling.md +58 -0
  51. package/node_modules/undici/docs/docs/best-practices/mocking-request.md +190 -0
  52. package/node_modules/undici/docs/docs/best-practices/proxy.md +127 -0
  53. package/node_modules/undici/docs/docs/best-practices/undici-vs-builtin-fetch.md +224 -0
  54. package/node_modules/undici/docs/docs/best-practices/writing-tests.md +63 -0
  55. package/node_modules/undici/index-fetch.js +65 -0
  56. package/node_modules/undici/index.d.ts +3 -0
  57. package/node_modules/undici/index.js +234 -0
  58. package/node_modules/undici/lib/api/abort-signal.js +59 -0
  59. package/node_modules/undici/lib/api/api-connect.js +110 -0
  60. package/node_modules/undici/lib/api/api-pipeline.js +252 -0
  61. package/node_modules/undici/lib/api/api-request.js +214 -0
  62. package/node_modules/undici/lib/api/api-stream.js +209 -0
  63. package/node_modules/undici/lib/api/api-upgrade.js +111 -0
  64. package/node_modules/undici/lib/api/index.js +7 -0
  65. package/node_modules/undici/lib/api/readable.js +580 -0
  66. package/node_modules/undici/lib/cache/memory-cache-store.js +279 -0
  67. package/node_modules/undici/lib/cache/sqlite-cache-store.js +467 -0
  68. package/node_modules/undici/lib/core/connect.js +153 -0
  69. package/node_modules/undici/lib/core/constants.js +143 -0
  70. package/node_modules/undici/lib/core/diagnostics.js +227 -0
  71. package/node_modules/undici/lib/core/errors.js +477 -0
  72. package/node_modules/undici/lib/core/request.js +485 -0
  73. package/node_modules/undici/lib/core/socks5-client.js +422 -0
  74. package/node_modules/undici/lib/core/socks5-utils.js +212 -0
  75. package/node_modules/undici/lib/core/symbols.js +75 -0
  76. package/node_modules/undici/lib/core/tree.js +160 -0
  77. package/node_modules/undici/lib/core/util.js +992 -0
  78. package/node_modules/undici/lib/dispatcher/agent.js +158 -0
  79. package/node_modules/undici/lib/dispatcher/balanced-pool.js +221 -0
  80. package/node_modules/undici/lib/dispatcher/client-h1.js +1746 -0
  81. package/node_modules/undici/lib/dispatcher/client-h2.js +1060 -0
  82. package/node_modules/undici/lib/dispatcher/client.js +668 -0
  83. package/node_modules/undici/lib/dispatcher/dispatcher-base.js +184 -0
  84. package/node_modules/undici/lib/dispatcher/dispatcher.js +48 -0
  85. package/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js +146 -0
  86. package/node_modules/undici/lib/dispatcher/fixed-queue.js +135 -0
  87. package/node_modules/undici/lib/dispatcher/h2c-client.js +51 -0
  88. package/node_modules/undici/lib/dispatcher/pool-base.js +214 -0
  89. package/node_modules/undici/lib/dispatcher/pool.js +118 -0
  90. package/node_modules/undici/lib/dispatcher/proxy-agent.js +319 -0
  91. package/node_modules/undici/lib/dispatcher/retry-agent.js +35 -0
  92. package/node_modules/undici/lib/dispatcher/round-robin-pool.js +137 -0
  93. package/node_modules/undici/lib/dispatcher/socks5-proxy-agent.js +260 -0
  94. package/node_modules/undici/lib/encoding/index.js +33 -0
  95. package/node_modules/undici/lib/global.js +59 -0
  96. package/node_modules/undici/lib/handler/cache-handler.js +776 -0
  97. package/node_modules/undici/lib/handler/cache-revalidation-handler.js +124 -0
  98. package/node_modules/undici/lib/handler/decorator-handler.js +67 -0
  99. package/node_modules/undici/lib/handler/deduplication-handler.js +460 -0
  100. package/node_modules/undici/lib/handler/redirect-handler.js +238 -0
  101. package/node_modules/undici/lib/handler/retry-handler.js +504 -0
  102. package/node_modules/undici/lib/handler/unwrap-handler.js +106 -0
  103. package/node_modules/undici/lib/handler/wrap-handler.js +105 -0
  104. package/node_modules/undici/lib/interceptor/cache.js +621 -0
  105. package/node_modules/undici/lib/interceptor/decompress.js +551 -0
  106. package/node_modules/undici/lib/interceptor/deduplicate.js +117 -0
  107. package/node_modules/undici/lib/interceptor/dns.js +571 -0
  108. package/node_modules/undici/lib/interceptor/dump.js +99 -0
  109. package/node_modules/undici/lib/interceptor/redirect.js +21 -0
  110. package/node_modules/undici/lib/interceptor/response-error.js +95 -0
  111. package/node_modules/undici/lib/interceptor/retry.js +19 -0
  112. package/node_modules/undici/lib/llhttp/.gitkeep +0 -0
  113. package/node_modules/undici/lib/llhttp/constants.d.ts +195 -0
  114. package/node_modules/undici/lib/llhttp/constants.js +531 -0
  115. package/node_modules/undici/lib/llhttp/llhttp-wasm.js +15 -0
  116. package/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js +15 -0
  117. package/node_modules/undici/lib/llhttp/utils.d.ts +2 -0
  118. package/node_modules/undici/lib/llhttp/utils.js +12 -0
  119. package/node_modules/undici/lib/mock/mock-agent.js +232 -0
  120. package/node_modules/undici/lib/mock/mock-call-history.js +248 -0
  121. package/node_modules/undici/lib/mock/mock-client.js +68 -0
  122. package/node_modules/undici/lib/mock/mock-errors.js +29 -0
  123. package/node_modules/undici/lib/mock/mock-interceptor.js +209 -0
  124. package/node_modules/undici/lib/mock/mock-pool.js +68 -0
  125. package/node_modules/undici/lib/mock/mock-symbols.js +32 -0
  126. package/node_modules/undici/lib/mock/mock-utils.js +486 -0
  127. package/node_modules/undici/lib/mock/pending-interceptors-formatter.js +43 -0
  128. package/node_modules/undici/lib/mock/snapshot-agent.js +353 -0
  129. package/node_modules/undici/lib/mock/snapshot-recorder.js +588 -0
  130. package/node_modules/undici/lib/mock/snapshot-utils.js +158 -0
  131. package/node_modules/undici/lib/util/cache.js +676 -0
  132. package/node_modules/undici/lib/util/date.js +670 -0
  133. package/node_modules/undici/lib/util/promise.js +28 -0
  134. package/node_modules/undici/lib/util/runtime-features.js +124 -0
  135. package/node_modules/undici/lib/util/stats.js +32 -0
  136. package/node_modules/undici/lib/util/timers.js +425 -0
  137. package/node_modules/undici/lib/web/cache/cache.js +864 -0
  138. package/node_modules/undici/lib/web/cache/cachestorage.js +152 -0
  139. package/node_modules/undici/lib/web/cache/util.js +45 -0
  140. package/node_modules/undici/lib/web/cookies/constants.js +12 -0
  141. package/node_modules/undici/lib/web/cookies/index.js +199 -0
  142. package/node_modules/undici/lib/web/cookies/parse.js +314 -0
  143. package/node_modules/undici/lib/web/cookies/util.js +352 -0
  144. package/node_modules/undici/lib/web/eventsource/eventsource-stream.js +494 -0
  145. package/node_modules/undici/lib/web/eventsource/eventsource.js +501 -0
  146. package/node_modules/undici/lib/web/eventsource/util.js +29 -0
  147. package/node_modules/undici/lib/web/fetch/LICENSE +21 -0
  148. package/node_modules/undici/lib/web/fetch/body.js +509 -0
  149. package/node_modules/undici/lib/web/fetch/constants.js +131 -0
  150. package/node_modules/undici/lib/web/fetch/data-url.js +596 -0
  151. package/node_modules/undici/lib/web/fetch/formdata-parser.js +586 -0
  152. package/node_modules/undici/lib/web/fetch/formdata.js +259 -0
  153. package/node_modules/undici/lib/web/fetch/global.js +40 -0
  154. package/node_modules/undici/lib/web/fetch/headers.js +719 -0
  155. package/node_modules/undici/lib/web/fetch/index.js +2413 -0
  156. package/node_modules/undici/lib/web/fetch/request.js +1115 -0
  157. package/node_modules/undici/lib/web/fetch/response.js +641 -0
  158. package/node_modules/undici/lib/web/fetch/util.js +1522 -0
  159. package/node_modules/undici/lib/web/infra/index.js +229 -0
  160. package/node_modules/undici/lib/web/subresource-integrity/Readme.md +9 -0
  161. package/node_modules/undici/lib/web/subresource-integrity/subresource-integrity.js +307 -0
  162. package/node_modules/undici/lib/web/webidl/index.js +1006 -0
  163. package/node_modules/undici/lib/web/websocket/connection.js +339 -0
  164. package/node_modules/undici/lib/web/websocket/constants.js +126 -0
  165. package/node_modules/undici/lib/web/websocket/events.js +331 -0
  166. package/node_modules/undici/lib/web/websocket/frame.js +133 -0
  167. package/node_modules/undici/lib/web/websocket/permessage-deflate.js +105 -0
  168. package/node_modules/undici/lib/web/websocket/receiver.js +507 -0
  169. package/node_modules/undici/lib/web/websocket/sender.js +109 -0
  170. package/node_modules/undici/lib/web/websocket/stream/websocketerror.js +104 -0
  171. package/node_modules/undici/lib/web/websocket/stream/websocketstream.js +496 -0
  172. package/node_modules/undici/lib/web/websocket/util.js +347 -0
  173. package/node_modules/undici/lib/web/websocket/websocket.js +758 -0
  174. package/node_modules/undici/package.json +152 -0
  175. package/node_modules/undici/scripts/strip-comments.js +10 -0
  176. package/node_modules/undici/types/README.md +6 -0
  177. package/node_modules/undici/types/agent.d.ts +32 -0
  178. package/node_modules/undici/types/api.d.ts +43 -0
  179. package/node_modules/undici/types/balanced-pool.d.ts +30 -0
  180. package/node_modules/undici/types/cache-interceptor.d.ts +179 -0
  181. package/node_modules/undici/types/cache.d.ts +36 -0
  182. package/node_modules/undici/types/client-stats.d.ts +15 -0
  183. package/node_modules/undici/types/client.d.ts +139 -0
  184. package/node_modules/undici/types/connector.d.ts +36 -0
  185. package/node_modules/undici/types/content-type.d.ts +21 -0
  186. package/node_modules/undici/types/cookies.d.ts +30 -0
  187. package/node_modules/undici/types/diagnostics-channel.d.ts +74 -0
  188. package/node_modules/undici/types/dispatcher.d.ts +275 -0
  189. package/node_modules/undici/types/env-http-proxy-agent.d.ts +22 -0
  190. package/node_modules/undici/types/errors.d.ts +177 -0
  191. package/node_modules/undici/types/eventsource.d.ts +66 -0
  192. package/node_modules/undici/types/fetch.d.ts +231 -0
  193. package/node_modules/undici/types/formdata.d.ts +114 -0
  194. package/node_modules/undici/types/global-dispatcher.d.ts +9 -0
  195. package/node_modules/undici/types/global-origin.d.ts +7 -0
  196. package/node_modules/undici/types/h2c-client.d.ts +73 -0
  197. package/node_modules/undici/types/handlers.d.ts +14 -0
  198. package/node_modules/undici/types/header.d.ts +160 -0
  199. package/node_modules/undici/types/index.d.ts +91 -0
  200. package/node_modules/undici/types/interceptors.d.ts +82 -0
  201. package/node_modules/undici/types/mock-agent.d.ts +68 -0
  202. package/node_modules/undici/types/mock-call-history.d.ts +111 -0
  203. package/node_modules/undici/types/mock-client.d.ts +27 -0
  204. package/node_modules/undici/types/mock-errors.d.ts +12 -0
  205. package/node_modules/undici/types/mock-interceptor.d.ts +94 -0
  206. package/node_modules/undici/types/mock-pool.d.ts +27 -0
  207. package/node_modules/undici/types/patch.d.ts +29 -0
  208. package/node_modules/undici/types/pool-stats.d.ts +19 -0
  209. package/node_modules/undici/types/pool.d.ts +41 -0
  210. package/node_modules/undici/types/proxy-agent.d.ts +29 -0
  211. package/node_modules/undici/types/readable.d.ts +68 -0
  212. package/node_modules/undici/types/retry-agent.d.ts +8 -0
  213. package/node_modules/undici/types/retry-handler.d.ts +125 -0
  214. package/node_modules/undici/types/round-robin-pool.d.ts +41 -0
  215. package/node_modules/undici/types/snapshot-agent.d.ts +109 -0
  216. package/node_modules/undici/types/socks5-proxy-agent.d.ts +25 -0
  217. package/node_modules/undici/types/util.d.ts +18 -0
  218. package/node_modules/undici/types/utility.d.ts +7 -0
  219. package/node_modules/undici/types/webidl.d.ts +347 -0
  220. package/node_modules/undici/types/websocket.d.ts +188 -0
  221. package/package.json +4 -2
@@ -0,0 +1,551 @@
1
+ 'use strict'
2
+
3
+ const { createInflate, createGunzip, createBrotliDecompress, createZstdDecompress } = require('node:zlib')
4
+ const { pipeline, Transform: TransformStream } = require('node:stream')
5
+ const { InvalidArgumentError, ResponseExceededMaxSizeError } = require('../core/errors')
6
+ const DecoratorHandler = require('../handler/decorator-handler')
7
+ const { runtimeFeatures } = require('../util/runtime-features')
8
+
9
+ /** @typedef {import('node:stream').Transform} Transform */
10
+ /** @typedef {import('node:stream').Transform} Controller */
11
+ /** @typedef {Transform&import('node:zlib').Zlib} DecompressorStream */
12
+
13
+ class DecompressController {
14
+ #onPause
15
+ #onResume
16
+ #onAbort
17
+ #paused = false
18
+
19
+ constructor (onPause, onResume, onAbort) {
20
+ this.#onPause = onPause
21
+ this.#onResume = onResume
22
+ this.#onAbort = onAbort
23
+ this.target = null
24
+ }
25
+
26
+ pause () {
27
+ if (this.#paused) {
28
+ return
29
+ }
30
+
31
+ this.#paused = true
32
+ this.#onPause()
33
+ }
34
+
35
+ resume () {
36
+ if (!this.#paused) {
37
+ return
38
+ }
39
+
40
+ this.#paused = false
41
+ this.#onResume()
42
+ }
43
+
44
+ abort (reason) {
45
+ this.target?.abort(reason)
46
+ this.#onAbort(reason)
47
+ }
48
+
49
+ get paused () { return this.#paused }
50
+ get aborted () { return this.target?.aborted ?? false }
51
+ get reason () { return this.target?.reason ?? null }
52
+ get rawHeaders () { return this.target?.rawHeaders ?? null }
53
+ set rawHeaders (value) {
54
+ if (this.target) {
55
+ this.target.rawHeaders = value
56
+ }
57
+ }
58
+
59
+ get rawTrailers () { return this.target?.rawTrailers ?? null }
60
+ set rawTrailers (value) {
61
+ if (this.target) {
62
+ this.target.rawTrailers = value
63
+ }
64
+ }
65
+ }
66
+
67
+ /** @type {Record<string, () => DecompressorStream>} */
68
+ const supportedEncodings = {
69
+ gzip: createGunzip,
70
+ 'x-gzip': createGunzip,
71
+ br: createBrotliDecompress,
72
+ deflate: createInflate,
73
+ compress: createInflate,
74
+ 'x-compress': createInflate,
75
+ ...(runtimeFeatures.has('zstd') ? { zstd: createZstdDecompress } : {})
76
+ }
77
+
78
+ const defaultSkipStatusCodes = /** @type {const} */ ([204, 304])
79
+ const defaultMaxSize = 0
80
+
81
+ /**
82
+ * Limits the output of one stage in a decompression chain.
83
+ * @param {number} maxSize - Maximum output size in bytes
84
+ * @returns {Transform}
85
+ */
86
+ function createMaxSizeLimiter (maxSize) {
87
+ let size = 0
88
+
89
+ return new TransformStream({
90
+ transform (chunk, _encoding, callback) {
91
+ const decompressedSize = size + chunk.length
92
+ if (decompressedSize > maxSize) {
93
+ callback(new ResponseExceededMaxSizeError(
94
+ `Decompressed response size (${decompressedSize}) exceeded maxSize (${maxSize})`
95
+ ))
96
+ return
97
+ }
98
+
99
+ size = decompressedSize
100
+ callback(null, chunk)
101
+ }
102
+ })
103
+ }
104
+
105
+ let warningEmitted = /** @type {boolean} */ (false)
106
+
107
+ /**
108
+ * @typedef {Object} DecompressHandlerOptions
109
+ * @property {number[]|Readonly<number[]>} [skipStatusCodes=[204, 304]] - List of status codes to skip decompression for
110
+ * @property {boolean} [skipErrorResponses] - Whether to skip decompression for error responses (status codes >= 400)
111
+ * @property {number} [maxSize=0] - Maximum decompressed response size in bytes. 0 disables the limit
112
+ */
113
+
114
+ class DecompressHandler extends DecoratorHandler {
115
+ /** @type {Transform[]} */
116
+ #decompressors = []
117
+ /** @type {Record<string, string | string[]> | undefined} */
118
+ #trailers
119
+ /** @type {Readonly<number[]>} */
120
+ #skipStatusCodes
121
+ /** @type {boolean} */
122
+ #skipErrorResponses
123
+ /** @type {number} */
124
+ #maxSize
125
+ /** @type {number} */
126
+ #decompressedSize = 0
127
+ /** @type {boolean} */
128
+ #terminated = false
129
+ /** @type {boolean} */
130
+ #inputEnded = false
131
+ /** @type {boolean} */
132
+ #inputBackpressured = false
133
+ /** @type {boolean} */
134
+ #upstreamPaused = false
135
+ /** @type {boolean} */
136
+ #draining = false
137
+ /** @type {boolean} */
138
+ #drainRequested = false
139
+ /** @type {boolean} */
140
+ #completionPending = false
141
+ /** @type {DecompressorStream | undefined} */
142
+ #finalDecompressor
143
+ /** @type {DecompressController} */
144
+ #controller
145
+
146
+ constructor (handler, { skipStatusCodes = defaultSkipStatusCodes, skipErrorResponses = true, maxSize = defaultMaxSize } = {}) {
147
+ if (!Number.isSafeInteger(maxSize) || maxSize < 0) {
148
+ throw new InvalidArgumentError('maxSize must be a non-negative integer')
149
+ }
150
+
151
+ super(handler)
152
+ this.#skipStatusCodes = skipStatusCodes
153
+ this.#skipErrorResponses = skipErrorResponses
154
+ this.#maxSize = maxSize
155
+ this.#controller = new DecompressController(
156
+ () => this.#onDownstreamPause(),
157
+ () => this.#onDownstreamResume(),
158
+ reason => {
159
+ if (this.#inputEnded && !this.#terminated) {
160
+ this.onResponseError(this.#controller, reason)
161
+ }
162
+ }
163
+ )
164
+ }
165
+
166
+ #onDownstreamPause () {
167
+ this.#pauseUpstream()
168
+ }
169
+
170
+ #onDownstreamResume () {
171
+ const drainWasDeferred = this.#draining
172
+ this.#drainOutput()
173
+ if (!drainWasDeferred) {
174
+ this.#resumeUpstreamIfNeeded()
175
+ this.#finishIfReady()
176
+ }
177
+ }
178
+
179
+ #pauseUpstream () {
180
+ if (!this.#upstreamPaused && !this.#terminated) {
181
+ this.#upstreamPaused = true
182
+ this.#controller.target?.pause()
183
+ }
184
+ }
185
+
186
+ #resumeUpstreamIfNeeded () {
187
+ if (this.#upstreamPaused && !this.#controller.paused && !this.#inputBackpressured) {
188
+ this.#upstreamPaused = false
189
+ if (!this.#inputEnded) {
190
+ this.#controller.target?.resume()
191
+ }
192
+ }
193
+ }
194
+
195
+ #drainOutput () {
196
+ if (this.#terminated || this.#controller.paused || !this.#finalDecompressor) {
197
+ return
198
+ }
199
+
200
+ if (this.#draining) {
201
+ this.#drainRequested = true
202
+ return
203
+ }
204
+
205
+ this.#draining = true
206
+ try {
207
+ do {
208
+ this.#drainRequested = false
209
+ let chunk
210
+ while (!this.#terminated && !this.#controller.paused && (chunk = this.#finalDecompressor.read()) !== null) {
211
+ if (this.#maxSize > 0) {
212
+ const decompressedSize = this.#decompressedSize + chunk.length
213
+ if (decompressedSize > this.#maxSize) {
214
+ this.#fail(new ResponseExceededMaxSizeError(
215
+ `Decompressed response size (${decompressedSize}) exceeded maxSize (${this.#maxSize})`
216
+ ))
217
+ return
218
+ }
219
+
220
+ this.#decompressedSize = decompressedSize
221
+ }
222
+
223
+ const result = super.onResponseData(this.#controller, chunk)
224
+ if (result === false && !this.#controller.paused) {
225
+ this.#controller.pause()
226
+ }
227
+ }
228
+ } while (this.#drainRequested && !this.#terminated && !this.#controller.paused)
229
+ } finally {
230
+ this.#draining = false
231
+ }
232
+
233
+ this.#resumeUpstreamIfNeeded()
234
+ this.#finishIfReady()
235
+ }
236
+
237
+ #finishIfReady () {
238
+ if (this.#terminated || !this.#completionPending || this.#controller.paused || this.#draining) {
239
+ return
240
+ }
241
+
242
+ this.#terminated = true
243
+ this.#cleanupDecompressors()
244
+ super.onResponseEnd(this.#controller, this.#trailers)
245
+ }
246
+
247
+ #onDecompressionEnd () {
248
+ if (this.#terminated) {
249
+ return
250
+ }
251
+
252
+ this.#completionPending = true
253
+ this.#drainOutput()
254
+ this.#finishIfReady()
255
+ }
256
+
257
+ /**
258
+ * Determines if decompression should be skipped based on encoding and status code
259
+ * @param {string} contentEncoding - Content-Encoding header value
260
+ * @param {number} statusCode - HTTP status code of the response
261
+ * @returns {boolean} - True if decompression should be skipped
262
+ */
263
+ #shouldSkipDecompression (contentEncoding, statusCode) {
264
+ if (!contentEncoding || statusCode < 200) return true
265
+ if (this.#skipStatusCodes.includes(statusCode)) return true
266
+ if (this.#skipErrorResponses && statusCode >= 400) return true
267
+ return false
268
+ }
269
+
270
+ /**
271
+ * Creates a chain of decompressors for multiple content encodings
272
+ *
273
+ * @param {string} encodings - Comma-separated list of content encodings
274
+ * @returns {Array<Transform>} - Array of decompressor and limiting streams
275
+ * @throws {Error} - If the number of content-encodings exceeds the maximum allowed
276
+ */
277
+ #createDecompressionChain (encodings) {
278
+ const parts = encodings.split(',')
279
+
280
+ // Limit the number of content-encodings to prevent resource exhaustion.
281
+ // CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206).
282
+ const maxContentEncodings = 5
283
+ if (parts.length > maxContentEncodings) {
284
+ throw new Error(`too many content-encodings in response: ${parts.length}, maximum allowed is ${maxContentEncodings}`)
285
+ }
286
+
287
+ /** @type {DecompressorStream[]} */
288
+ const decompressors = []
289
+
290
+ for (let i = parts.length - 1; i >= 0; i--) {
291
+ const encoding = parts[i].trim()
292
+ if (!encoding) continue
293
+
294
+ if (!supportedEncodings[encoding]) {
295
+ decompressors.length = 0 // Clear if unsupported encoding
296
+ return decompressors // Unsupported encoding
297
+ }
298
+
299
+ decompressors.push(supportedEncodings[encoding]())
300
+ }
301
+
302
+ if (decompressors.length < 2) {
303
+ return decompressors
304
+ }
305
+
306
+ /** @type {Transform[]} */
307
+ const streams = []
308
+ for (let i = 0; i < decompressors.length; i++) {
309
+ streams.push(decompressors[i])
310
+ if (i < decompressors.length - 1 && this.#maxSize > 0) {
311
+ streams.push(createMaxSizeLimiter(this.#maxSize))
312
+ }
313
+ }
314
+
315
+ return streams
316
+ }
317
+
318
+ /**
319
+ * Stops decompression and reports an error.
320
+ * @param {Error} error - The decompression error
321
+ * @returns {void}
322
+ */
323
+ #fail (error) {
324
+ if (this.#terminated) {
325
+ return
326
+ }
327
+
328
+ if (this.#inputEnded) {
329
+ // The request is already marked complete once the compressed input ends,
330
+ // so controller.abort() can no longer propagate decoder flush errors.
331
+ this.onResponseError(this.#controller, error)
332
+ } else {
333
+ this.#controller.abort(error)
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Sets up event handlers for the final decompressor stream.
339
+ * @param {DecompressorStream} decompressor - The decompressor stream
340
+ * @returns {void}
341
+ */
342
+ #setupDecompressorEvents (decompressor) {
343
+ this.#finalDecompressor = decompressor
344
+ decompressor.on('readable', () => this.#drainOutput())
345
+ decompressor.on('error', (error) => this.#fail(error))
346
+ }
347
+
348
+ /**
349
+ * Sets up event handling for a single decompressor
350
+ * @returns {void}
351
+ */
352
+ #setupSingleDecompressor () {
353
+ const decompressor = this.#decompressors[0]
354
+ this.#setupDecompressorEvents(decompressor)
355
+
356
+ decompressor.on('end', () => this.#onDecompressionEnd())
357
+ }
358
+
359
+ /**
360
+ * Sets up event handling for multiple chained decompressors using pipeline
361
+ * @returns {void}
362
+ */
363
+ #setupMultipleDecompressors () {
364
+ const lastDecompressor = this.#decompressors[this.#decompressors.length - 1]
365
+ this.#setupDecompressorEvents(lastDecompressor)
366
+
367
+ pipeline(this.#decompressors, (err) => {
368
+ if (this.#terminated) {
369
+ return
370
+ }
371
+
372
+ if (err) {
373
+ this.#fail(err)
374
+ return
375
+ }
376
+
377
+ this.#onDecompressionEnd()
378
+ })
379
+ }
380
+
381
+ #setupInputBackpressure () {
382
+ const decompressor = this.#decompressors[0]
383
+ decompressor.on('drain', () => {
384
+ if (this.#terminated) {
385
+ return
386
+ }
387
+
388
+ this.#inputBackpressured = false
389
+ if (!this.#controller.paused) {
390
+ this.#drainOutput()
391
+ this.#resumeUpstreamIfNeeded()
392
+ }
393
+ })
394
+ }
395
+
396
+ /**
397
+ * Cleans up decompressor references to prevent memory leaks
398
+ * @returns {void}
399
+ */
400
+ #cleanupDecompressors () {
401
+ this.#decompressors.length = 0
402
+ this.#finalDecompressor = undefined
403
+ }
404
+
405
+ onRequestStart (controller, context) {
406
+ this.#controller.target = controller
407
+ return super.onRequestStart(this.#controller, context)
408
+ }
409
+
410
+ onRequestUpgrade (controller, statusCode, headers, socket) {
411
+ return super.onRequestUpgrade(this.#controller, statusCode, headers, socket)
412
+ }
413
+
414
+ /**
415
+ * @param {Controller} controller
416
+ * @param {number} statusCode
417
+ * @param {Record<string, string | string[] | undefined>} headers
418
+ * @param {string} statusMessage
419
+ * @returns {void}
420
+ */
421
+ onResponseStart (controller, statusCode, headers, statusMessage) {
422
+ const contentEncoding = headers['content-encoding']
423
+
424
+ // If content encoding is not supported or status code is in skip list
425
+ if (this.#shouldSkipDecompression(contentEncoding, statusCode)) {
426
+ return super.onResponseStart(this.#controller, statusCode, headers, statusMessage)
427
+ }
428
+
429
+ const decompressors = this.#createDecompressionChain(contentEncoding.toLowerCase())
430
+
431
+ if (decompressors.length === 0) {
432
+ this.#cleanupDecompressors()
433
+ return super.onResponseStart(this.#controller, statusCode, headers, statusMessage)
434
+ }
435
+
436
+ this.#decompressors = decompressors
437
+
438
+ // Remove compression headers since we're decompressing
439
+ const { 'content-encoding': _, 'content-length': __, ...newHeaders } = headers
440
+
441
+ if (this.#controller.rawHeaders) {
442
+ const rawHeaders = this.#controller.rawHeaders
443
+
444
+ if (Array.isArray(rawHeaders)) {
445
+ const filteredHeaders = []
446
+ for (let i = 0; i < rawHeaders.length; i += 2) {
447
+ const headerName = rawHeaders[i]
448
+ const name = Buffer.isBuffer(headerName) ? headerName.toString('latin1') : `${headerName}`
449
+ const lowerName = name.toLowerCase()
450
+
451
+ if (lowerName === 'content-encoding' || lowerName === 'content-length') {
452
+ continue
453
+ }
454
+
455
+ filteredHeaders.push(rawHeaders[i], rawHeaders[i + 1])
456
+ }
457
+ rawHeaders.splice(0, rawHeaders.length, ...filteredHeaders)
458
+ } else if (typeof rawHeaders === 'object') {
459
+ for (const name of Object.keys(rawHeaders)) {
460
+ const lowerName = name.toLowerCase()
461
+ if (lowerName === 'content-encoding' || lowerName === 'content-length') {
462
+ delete rawHeaders[name]
463
+ }
464
+ }
465
+ }
466
+ }
467
+
468
+ this.#setupInputBackpressure()
469
+ if (this.#decompressors.length === 1) {
470
+ this.#setupSingleDecompressor()
471
+ } else {
472
+ this.#setupMultipleDecompressors()
473
+ }
474
+
475
+ return super.onResponseStart(this.#controller, statusCode, newHeaders, statusMessage)
476
+ }
477
+
478
+ /**
479
+ * @param {Controller} controller
480
+ * @param {Buffer} chunk
481
+ * @returns {void}
482
+ */
483
+ onResponseData (controller, chunk) {
484
+ if (this.#decompressors.length > 0) {
485
+ if (!this.#decompressors[0].write(chunk)) {
486
+ this.#inputBackpressured = true
487
+ this.#pauseUpstream()
488
+ }
489
+ return
490
+ }
491
+ return super.onResponseData(this.#controller, chunk)
492
+ }
493
+
494
+ /**
495
+ * @param {Controller} controller
496
+ * @param {Record<string, string | string[]> | undefined} trailers
497
+ * @returns {void}
498
+ */
499
+ onResponseEnd (controller, trailers) {
500
+ if (this.#decompressors.length > 0) {
501
+ this.#inputEnded = true
502
+ this.#trailers = trailers
503
+ this.#decompressors[0].end()
504
+ return
505
+ }
506
+ return super.onResponseEnd(this.#controller, trailers)
507
+ }
508
+
509
+ /**
510
+ * @param {Controller} controller
511
+ * @param {Error} err
512
+ * @returns {void}
513
+ */
514
+ onResponseError (controller, err) {
515
+ if (this.#terminated) {
516
+ return
517
+ }
518
+
519
+ this.#terminated = true
520
+ for (const decompressor of this.#decompressors) {
521
+ decompressor.destroy()
522
+ }
523
+ this.#cleanupDecompressors()
524
+ super.onResponseError(this.#controller, err)
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Creates a decompression interceptor for HTTP responses
530
+ * @param {DecompressHandlerOptions} [options] - Options for the interceptor
531
+ * @returns {Function} - Interceptor function
532
+ */
533
+ function createDecompressInterceptor (options = {}) {
534
+ // Emit experimental warning only once
535
+ if (!warningEmitted) {
536
+ process.emitWarning(
537
+ 'DecompressInterceptor is experimental and subject to change',
538
+ 'ExperimentalWarning'
539
+ )
540
+ warningEmitted = true
541
+ }
542
+
543
+ return (dispatch) => {
544
+ return (opts, handler) => {
545
+ const decompressHandler = new DecompressHandler(handler, options)
546
+ return dispatch(opts, decompressHandler)
547
+ }
548
+ }
549
+ }
550
+
551
+ module.exports = createDecompressInterceptor
@@ -0,0 +1,117 @@
1
+ 'use strict'
2
+
3
+ const diagnosticsChannel = require('node:diagnostics_channel')
4
+ const util = require('../core/util')
5
+ const DeduplicationHandler = require('../handler/deduplication-handler')
6
+ const { normalizeHeaders, makeCacheKey, makeDeduplicationKey } = require('../util/cache.js')
7
+
8
+ const pendingRequestsChannel = diagnosticsChannel.channel('undici:request:pending-requests')
9
+
10
+ /**
11
+ * @param {import('../../types/interceptors.d.ts').default.DeduplicateInterceptorOpts} [opts]
12
+ * @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor}
13
+ */
14
+ module.exports = (opts = {}) => {
15
+ const {
16
+ methods = ['GET'],
17
+ skipHeaderNames = [],
18
+ excludeHeaderNames = [],
19
+ maxBufferSize = 5 * 1024 * 1024
20
+ } = opts
21
+
22
+ if (typeof opts !== 'object' || opts === null) {
23
+ throw new TypeError(`expected type of opts to be an Object, got ${opts === null ? 'null' : typeof opts}`)
24
+ }
25
+
26
+ if (!Array.isArray(methods)) {
27
+ throw new TypeError(`expected opts.methods to be an array, got ${typeof methods}`)
28
+ }
29
+
30
+ for (const method of methods) {
31
+ if (!util.safeHTTPMethods.includes(method)) {
32
+ throw new TypeError(`expected opts.methods to only contain safe HTTP methods, got ${method}`)
33
+ }
34
+ }
35
+
36
+ if (!Array.isArray(skipHeaderNames)) {
37
+ throw new TypeError(`expected opts.skipHeaderNames to be an array, got ${typeof skipHeaderNames}`)
38
+ }
39
+
40
+ if (!Array.isArray(excludeHeaderNames)) {
41
+ throw new TypeError(`expected opts.excludeHeaderNames to be an array, got ${typeof excludeHeaderNames}`)
42
+ }
43
+
44
+ if (!Number.isFinite(maxBufferSize) || maxBufferSize <= 0) {
45
+ throw new TypeError(`expected opts.maxBufferSize to be a positive finite number, got ${maxBufferSize}`)
46
+ }
47
+
48
+ // Convert to lowercase Set for case-insensitive header matching
49
+ const skipHeaderNamesSet = new Set(skipHeaderNames.map(name => name.toLowerCase()))
50
+
51
+ // Convert to lowercase Set for case-insensitive header exclusion from deduplication key
52
+ const excludeHeaderNamesSet = new Set(excludeHeaderNames.map(name => name.toLowerCase()))
53
+
54
+ /**
55
+ * Map of pending requests for deduplication
56
+ * @type {Map<string, DeduplicationHandler>}
57
+ */
58
+ const pendingRequests = new Map()
59
+
60
+ return dispatch => {
61
+ return (opts, handler) => {
62
+ if (!opts.origin || methods.includes(opts.method) === false) {
63
+ return dispatch(opts, handler)
64
+ }
65
+
66
+ opts = {
67
+ ...opts,
68
+ headers: normalizeHeaders(opts)
69
+ }
70
+
71
+ // Skip deduplication if request contains any of the specified headers
72
+ if (skipHeaderNamesSet.size > 0) {
73
+ for (const headerName of Object.keys(opts.headers)) {
74
+ if (skipHeaderNamesSet.has(headerName.toLowerCase())) {
75
+ return dispatch(opts, handler)
76
+ }
77
+ }
78
+ }
79
+
80
+ const cacheKey = makeCacheKey(opts)
81
+ const dedupeKey = makeDeduplicationKey(cacheKey, excludeHeaderNamesSet)
82
+
83
+ // Check if there's already a pending request for this key
84
+ const pendingHandler = pendingRequests.get(dedupeKey)
85
+ if (pendingHandler) {
86
+ // Add this handler to the waiting list when safe.
87
+ // If body streaming has already started, this request must be sent independently.
88
+ if (pendingHandler.addWaitingHandler(handler)) {
89
+ return true
90
+ }
91
+
92
+ return dispatch(opts, handler)
93
+ }
94
+
95
+ // Create a new deduplication handler
96
+ const deduplicationHandler = new DeduplicationHandler(
97
+ handler,
98
+ () => {
99
+ // Clean up when request completes
100
+ pendingRequests.delete(dedupeKey)
101
+ if (pendingRequestsChannel.hasSubscribers) {
102
+ pendingRequestsChannel.publish({ size: pendingRequests.size, key: dedupeKey, type: 'removed' })
103
+ }
104
+ },
105
+ maxBufferSize
106
+ )
107
+
108
+ // Register the pending request
109
+ pendingRequests.set(dedupeKey, deduplicationHandler)
110
+ if (pendingRequestsChannel.hasSubscribers) {
111
+ pendingRequestsChannel.publish({ size: pendingRequests.size, key: dedupeKey, type: 'added' })
112
+ }
113
+
114
+ return dispatch(opts, deduplicationHandler)
115
+ }
116
+ }
117
+ }