@suveren/gateway 0.29.1 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/integrations/calendar.json +3 -0
- package/content/integrations/crm.json +10 -0
- package/content/integrations/deploy-github.json +1 -0
- package/content/integrations/erp.json +4 -0
- package/content/integrations/gmail.json +4 -0
- package/content/integrations/linkedin.json +4 -0
- package/content/integrations/mail.json +1 -0
- package/content/integrations/mollie.json +5 -0
- package/content/integrations/records.json +4 -0
- package/dist/control-plane/index.mjs +151 -32
- package/dist/mcp-server/http.mjs +156 -37
- package/dist/ui/assets/index-B7qy0B9J.js +123 -0
- package/dist/ui/assets/index-DyZY5sLI.css +1 -0
- package/dist/ui/index.html +2 -2
- package/node_modules/qs/.github/THREAT_MODEL.md +4 -2
- package/node_modules/qs/CHANGELOG.md +38 -14
- package/node_modules/qs/README.md +31 -3
- package/node_modules/qs/dist/qs.js +20 -20
- package/node_modules/qs/lib/parse.js +94 -4
- package/node_modules/qs/lib/stringify.js +4 -4
- package/node_modules/qs/lib/utils.js +130 -6
- package/node_modules/qs/package.json +4 -4
- package/node_modules/qs/test/parse.js +174 -0
- package/node_modules/qs/test/stringify.js +263 -33
- package/node_modules/qs/test/utils.js +184 -0
- package/node_modules/undici/docs/docs/api/Client.md +2 -0
- package/node_modules/undici/lib/core/connect.js +10 -4
- package/node_modules/undici/lib/core/symbols.js +1 -0
- package/node_modules/undici/lib/dispatcher/agent.js +6 -2
- package/node_modules/undici/lib/dispatcher/client-h1.js +0 -2
- package/node_modules/undici/lib/dispatcher/client-h2.js +0 -4
- package/node_modules/undici/lib/dispatcher/client.js +3 -2
- package/node_modules/undici/lib/dispatcher/dispatcher-base.js +12 -0
- package/node_modules/undici/lib/handler/cache-handler.js +48 -0
- package/node_modules/undici/lib/handler/deduplication-handler.js +57 -13
- package/node_modules/undici/lib/handler/redirect-handler.js +8 -1
- package/node_modules/undici/lib/interceptor/decompress.js +15 -5
- package/node_modules/undici/lib/web/cookies/index.js +3 -1
- package/node_modules/undici/lib/web/cookies/parse.js +138 -137
- package/node_modules/undici/lib/web/cookies/util.js +21 -2
- package/node_modules/undici/lib/web/eventsource/eventsource-stream.js +90 -9
- package/node_modules/undici/lib/web/eventsource/eventsource.js +4 -1
- package/node_modules/undici/lib/web/fetch/index.js +3 -3
- package/node_modules/undici/lib/web/infra/index.js +14 -0
- package/node_modules/undici/lib/web/subresource-integrity/subresource-integrity.js +5 -5
- package/node_modules/undici/lib/web/websocket/receiver.js +13 -0
- package/node_modules/undici/package.json +1 -1
- package/node_modules/undici/types/client.d.ts +14 -0
- package/package.json +1 -1
- package/dist/ui/assets/index-DUIlMVlP.css +0 -1
- package/dist/ui/assets/index-wtX8LuiU.js +0 -123
|
@@ -106,6 +106,146 @@ test('merge()', function (t) {
|
|
|
106
106
|
s2t.end();
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
+
st.test('with strictMerge, repeated conflicts append instead of nesting a new wrapper', function (s2t) {
|
|
110
|
+
var opts = { strictMerge: true };
|
|
111
|
+
var wrapped = utils.merge({ foo: 'bar' }, 'baz', opts);
|
|
112
|
+
s2t.deepEqual(wrapped, [{ foo: 'bar' }, 'baz'], 'first conflict wraps in an array');
|
|
113
|
+
|
|
114
|
+
var withObject = utils.merge(wrapped, { qux: 'quux' }, opts);
|
|
115
|
+
s2t.deepEqual(
|
|
116
|
+
withObject,
|
|
117
|
+
{ 0: { foo: 'bar' }, 1: 'baz', qux: 'quux' },
|
|
118
|
+
'merging an object converts the wrapper to an object'
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
var second = utils.merge(withObject, 'corge', opts);
|
|
122
|
+
s2t.deepEqual(
|
|
123
|
+
second,
|
|
124
|
+
{ 0: { foo: 'bar' }, 1: 'baz', 2: 'corge', qux: 'quux' },
|
|
125
|
+
'a second conflict appends at the next index'
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
var third = utils.merge(second, 'grault', opts);
|
|
129
|
+
s2t.deepEqual(
|
|
130
|
+
third,
|
|
131
|
+
{ 0: { foo: 'bar' }, 1: 'baz', 2: 'corge', 3: 'grault', qux: 'quux' },
|
|
132
|
+
'a third conflict appends again, so nesting depth stays constant'
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
s2t.end();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
st.test('with strictMerge, a caller-supplied array is still wrapped, not appended to', function (s2t) {
|
|
139
|
+
var merged = utils.merge({ 0: 'a', 1: 'b' }, 'c', { strictMerge: true });
|
|
140
|
+
s2t.deepEqual(merged, [{ 0: 'a', 1: 'b' }, 'c'], 'an unmarked object with numeric keys is wrapped');
|
|
141
|
+
s2t.end();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
st.test('with strictMerge, throws instead of appending a conflict past arrayLimit with throwOnLimitExceeded', function (s2t) {
|
|
145
|
+
var appendConflict = function (options) {
|
|
146
|
+
var wrapped = utils.merge({ foo: 'bar' }, 'baz', { strictMerge: true });
|
|
147
|
+
var container = utils.merge(wrapped, { qux: 'quux' }, { strictMerge: true });
|
|
148
|
+
return utils.merge(container, 'corge', options);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
s2t['throws'](
|
|
152
|
+
function () { appendConflict({ strictMerge: true, arrayLimit: 1, throwOnLimitExceeded: true }); },
|
|
153
|
+
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
|
154
|
+
'appending to a two-element container past arrayLimit throws'
|
|
155
|
+
);
|
|
156
|
+
s2t['throws'](
|
|
157
|
+
function () { appendConflict({ strictMerge: true, arrayLimit: 2, throwOnLimitExceeded: true }); },
|
|
158
|
+
new RangeError('Array limit exceeded. Only 2 elements allowed in an array.'),
|
|
159
|
+
'uses the plural message when arrayLimit is not 1'
|
|
160
|
+
);
|
|
161
|
+
s2t.deepEqual(
|
|
162
|
+
appendConflict({ strictMerge: true, arrayLimit: 3, throwOnLimitExceeded: true }),
|
|
163
|
+
{ 0: { foo: 'bar' }, 1: 'baz', 2: 'corge', qux: 'quux' },
|
|
164
|
+
'appends while the container stays within arrayLimit'
|
|
165
|
+
);
|
|
166
|
+
s2t.deepEqual(
|
|
167
|
+
appendConflict({ strictMerge: true, arrayLimit: 1 }),
|
|
168
|
+
{ 0: { foo: 'bar' }, 1: 'baz', 2: 'corge', qux: 'quux' },
|
|
169
|
+
'still appends past arrayLimit without throwOnLimitExceeded, since the container is already an object'
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
s2t.end();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
st.test('with strictMerge, a conflict appends after numeric keys already in its container', function (s2t) {
|
|
176
|
+
var opts = { strictMerge: true };
|
|
177
|
+
|
|
178
|
+
var container = utils.merge(utils.merge({ foo: 'bar' }, 'baz', opts), { qux: 'quux' }, opts);
|
|
179
|
+
var withIndex = utils.merge(container, { 2: 'keep' }, opts);
|
|
180
|
+
s2t.deepEqual(
|
|
181
|
+
utils.merge(withIndex, 'corge', opts),
|
|
182
|
+
{ 0: { foo: 'bar' }, 1: 'baz', 2: 'keep', 3: 'corge', qux: 'quux' },
|
|
183
|
+
'a numeric key merged into the container is not overwritten'
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
var sparse = [];
|
|
187
|
+
sparse[2] = 'keep';
|
|
188
|
+
var extended = utils.merge(utils.merge({ foo: 'bar' }, 'baz', opts), sparse, opts);
|
|
189
|
+
s2t.deepEqual(extended, [{ foo: 'bar' }, 'baz', 'keep'], 'an indexed value extends the container while it is an array');
|
|
190
|
+
var converted = utils.merge(extended, { qux: 'quux' }, opts);
|
|
191
|
+
s2t.deepEqual(
|
|
192
|
+
utils.merge(converted, 'corge', opts),
|
|
193
|
+
{ 0: { foo: 'bar' }, 1: 'baz', 2: 'keep', 3: 'corge', qux: 'quux' },
|
|
194
|
+
'converting the container to an object keeps its last index'
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
s2t.end();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
st.test('enforces arrayLimit on the two-element array built for colliding values', function (s2t) {
|
|
201
|
+
s2t['throws'](
|
|
202
|
+
function () { utils.merge({ foo: 'bar' }, 'baz', { strictMerge: true, arrayLimit: 1, throwOnLimitExceeded: true }); },
|
|
203
|
+
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
|
204
|
+
'a strictMerge conflict throws at arrayLimit 1'
|
|
205
|
+
);
|
|
206
|
+
s2t['throws'](
|
|
207
|
+
function () { utils.merge('a', 'b', { arrayLimit: 0, throwOnLimitExceeded: true }); },
|
|
208
|
+
new RangeError('Array limit exceeded. Only 0 elements allowed in an array.'),
|
|
209
|
+
'two colliding primitives throw at arrayLimit 0'
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
var conflict = utils.merge({ foo: 'bar' }, 'baz', { strictMerge: true, arrayLimit: 1 });
|
|
213
|
+
s2t.ok(utils.isOverflow(conflict), 'without throwOnLimitExceeded, a strictMerge conflict converts to an overflow object');
|
|
214
|
+
s2t.deepEqual(conflict, { 0: { foo: 'bar' }, 1: 'baz' }, 'holding both values');
|
|
215
|
+
|
|
216
|
+
var pair = utils.merge('a', 'b', { arrayLimit: 1 });
|
|
217
|
+
s2t.ok(utils.isOverflow(pair), 'without throwOnLimitExceeded, two colliding primitives convert to an overflow object');
|
|
218
|
+
s2t.deepEqual(pair, { 0: 'a', 1: 'b' }, 'holding both values');
|
|
219
|
+
|
|
220
|
+
s2t.deepEqual(
|
|
221
|
+
utils.merge({ foo: 'bar' }, 'baz', { strictMerge: true, arrayLimit: 2, throwOnLimitExceeded: true }),
|
|
222
|
+
[{ foo: 'bar' }, 'baz'],
|
|
223
|
+
'a strictMerge conflict within arrayLimit is still an array'
|
|
224
|
+
);
|
|
225
|
+
s2t.deepEqual(
|
|
226
|
+
utils.merge('a', 'b', { arrayLimit: 2, throwOnLimitExceeded: true }),
|
|
227
|
+
['a', 'b'],
|
|
228
|
+
'two colliding primitives within arrayLimit are still an array'
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
s2t.end();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
st.test('seeds an overflow container index from the keys it already holds', function (s2t) {
|
|
235
|
+
var overflow = utils.combine(['a'], 'b', 0, false);
|
|
236
|
+
s2t.ok(utils.isOverflow(overflow), 'the source is an overflow object');
|
|
237
|
+
|
|
238
|
+
var merged = utils.merge({ 2: 'keep', z: 'zz' }, overflow);
|
|
239
|
+
s2t.ok(utils.isOverflow(merged), 'the result is marked as overflow');
|
|
240
|
+
s2t.deepEqual(
|
|
241
|
+
utils.merge(merged, 'c'),
|
|
242
|
+
{ 0: 'a', 1: 'b', 2: 'keep', 3: 'c', z: 'zz' },
|
|
243
|
+
'the next value lands after the highest numeric key the container already held'
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
s2t.end();
|
|
247
|
+
});
|
|
248
|
+
|
|
109
249
|
st.test('merges overflow object into primitive', function (s2t) {
|
|
110
250
|
// Create an overflow object via combine: 2 elements (indices 0-1) with limit 0
|
|
111
251
|
var overflow = utils.combine(['a'], 'b', 0, false);
|
|
@@ -441,6 +581,50 @@ test('decode', function (t) {
|
|
|
441
581
|
'decodes a string in iso-8859-1'
|
|
442
582
|
);
|
|
443
583
|
|
|
584
|
+
t.test('strings with no percent sign', function (st) {
|
|
585
|
+
st.equal(utils.decode(''), '', 'empty string');
|
|
586
|
+
st.equal(utils.decode('abc'), 'abc', 'returns a plain string unchanged');
|
|
587
|
+
st.equal(utils.decode('abc', null, 'iso-8859-1'), 'abc', 'returns a plain string unchanged in iso-8859-1');
|
|
588
|
+
st.equal(utils.decode('a++b+'), 'a b ', 'decodes every + to a space');
|
|
589
|
+
st.equal(utils.decode('a++b+', null, 'iso-8859-1'), 'a b ', 'decodes every + to a space in iso-8859-1');
|
|
590
|
+
st.equal(utils.decode('\uD800x\uDC00'), '\uD800x\uDC00', 'leaves lone surrogates unchanged');
|
|
591
|
+
st.equal(utils.decode('😀+'), '😀 ', 'leaves surrogate pairs unchanged');
|
|
592
|
+
st.equal(utils.decode('a=b&c'), 'a=b&c', 'leaves delimiters unchanged');
|
|
593
|
+
|
|
594
|
+
st.end();
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
t.test('strings with a percent sign', function (st) {
|
|
598
|
+
st.equal(utils.decode('a+%20b'), 'a b', 'decodes both + and an escape');
|
|
599
|
+
st.equal(utils.decode('%E9+x', null, 'iso-8859-1'), 'é x', 'decodes both + and an escape in iso-8859-1');
|
|
600
|
+
st.equal(utils.decode('a+%'), 'a %', 'decodes + even when the escape is malformed');
|
|
601
|
+
st.equal(utils.decode('a+%E0%A4%A'), 'a %E0%A4%A', 'decodes + even when the escape is truncated');
|
|
602
|
+
|
|
603
|
+
st.end();
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
t.test('non-strings', function (st) {
|
|
607
|
+
forEach(v.nullPrimitives, function (nullish) {
|
|
608
|
+
st['throws'](
|
|
609
|
+
function () { utils.decode(nullish); },
|
|
610
|
+
TypeError,
|
|
611
|
+
inspect(nullish) + ' is not a string'
|
|
612
|
+
);
|
|
613
|
+
});
|
|
614
|
+
st['throws'](
|
|
615
|
+
function () { utils.decode(['a']); },
|
|
616
|
+
TypeError,
|
|
617
|
+
'an array is not a string'
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
/* eslint no-new-wrappers: 0 */
|
|
621
|
+
st.equal(utils.decode(new String('ab')), 'ab', 'a boxed string decodes to a primitive');
|
|
622
|
+
st.equal(utils.decode(new String('a+b')), 'a b', 'a boxed string with + decodes to a primitive');
|
|
623
|
+
st.equal(utils.decode(new String('a%20b')), 'a b', 'a boxed string with an escape decodes to a primitive');
|
|
624
|
+
|
|
625
|
+
st.end();
|
|
626
|
+
});
|
|
627
|
+
|
|
444
628
|
t.end();
|
|
445
629
|
});
|
|
446
630
|
|
|
@@ -27,6 +27,8 @@ Returns: `Client`
|
|
|
27
27
|
* **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options.
|
|
28
28
|
* **maxFragments** `number` (optional) - Default: `131072` - Maximum number of fragments in a message. Set to 0 to disable the limit.
|
|
29
29
|
* **maxPayloadSize** `number` (optional) - Default: `134217728` (128 MB) - Maximum allowed payload size in bytes for WebSocket messages. Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. Set to 0 to disable the limit.
|
|
30
|
+
* **eventSource** `EventSourceOptions` (optional) - EventSource-specific configuration options.
|
|
31
|
+
* **maxEventSize** `number` (optional) - The maximum total bytes of data and metadata field values retained for a pending EventSource event. Each unfinished line is separately limited to this value plus seven syntax bytes. The number of `data:` lines per event and input chunks buffered for an unfinished line are each limited to `min(131072, max(1024, floor(maxEventSize / 128)))` to bound object overhead. Set to `0` to disable these limits. Default: `buffer.kStringMaxLength`.
|
|
30
32
|
* **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections.
|
|
31
33
|
* **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
|
|
32
34
|
* **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. **Security Warning:** Disabling this option can expose your application to HTTP Request Smuggling attacks, where mismatched content-length headers cause servers and proxies to interpret request boundaries differently. This can lead to cache poisoning, credential hijacking, and bypassing security controls. Only disable this in controlled environments where you fully trust the request source.
|
|
@@ -41,16 +41,22 @@ const SessionCache = class WeakSessionCache {
|
|
|
41
41
|
if (this._sessionCache.has(sessionKey)) {
|
|
42
42
|
this._sessionCache.delete(sessionKey)
|
|
43
43
|
} else if (this._sessionCache.size >= this._maxCachedSessions) {
|
|
44
|
+
// Make room for the new session: prefer an entry whose session has
|
|
45
|
+
// already been collected, otherwise evict the oldest one.
|
|
46
|
+
let evicted = false
|
|
44
47
|
for (const [key, ref] of this._sessionCache) {
|
|
45
48
|
if (ref.deref() === undefined) {
|
|
46
49
|
this._sessionCache.delete(key)
|
|
47
|
-
|
|
50
|
+
evicted = true
|
|
51
|
+
break
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
if (!evicted) {
|
|
56
|
+
const oldest = this._sessionCache.keys().next()
|
|
57
|
+
if (!oldest.done) {
|
|
58
|
+
this._sessionCache.delete(oldest.value)
|
|
59
|
+
}
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
|
|
@@ -22,7 +22,7 @@ function defaultFactory (origin, opts) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
class Agent extends DispatcherBase {
|
|
25
|
-
constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, ...options } = {}) {
|
|
25
|
+
constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, tls, ...options } = {}) {
|
|
26
26
|
if (typeof factory !== 'function') {
|
|
27
27
|
throw new InvalidArgumentError('factory must be a function.')
|
|
28
28
|
}
|
|
@@ -41,7 +41,11 @@ class Agent extends DispatcherBase {
|
|
|
41
41
|
connect = { ...connect }
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
if (tls && typeof tls !== 'function') {
|
|
45
|
+
tls = { ...tls }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this[kOptions] = { ...util.deepClone(options), maxOrigins, connect, tls }
|
|
45
49
|
this[kFactory] = factory
|
|
46
50
|
this[kClients] = new Map()
|
|
47
51
|
this[kOrigins] = new Set()
|
|
@@ -928,8 +928,6 @@ function connectH1 (client, socket) {
|
|
|
928
928
|
}
|
|
929
929
|
|
|
930
930
|
function onHttpSocketError (err) {
|
|
931
|
-
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
|
932
|
-
|
|
933
931
|
const parser = this[kParser]
|
|
934
932
|
|
|
935
933
|
// On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
|
|
@@ -293,8 +293,6 @@ function onHttp2SendPing (session) {
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
function onHttp2SessionError (err) {
|
|
296
|
-
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
|
297
|
-
|
|
298
296
|
this[kSocket][kError] = err
|
|
299
297
|
this[kClient][kOnError](err)
|
|
300
298
|
}
|
|
@@ -436,8 +434,6 @@ function onHttp2SocketClose () {
|
|
|
436
434
|
}
|
|
437
435
|
|
|
438
436
|
function onHttp2SocketError (err) {
|
|
439
|
-
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
|
440
|
-
|
|
441
437
|
this[kError] = err
|
|
442
438
|
|
|
443
439
|
this[kClient][kOnError](err)
|
|
@@ -115,7 +115,8 @@ class Client extends DispatcherBase {
|
|
|
115
115
|
initialWindowSize,
|
|
116
116
|
connectionWindowSize,
|
|
117
117
|
pingInterval,
|
|
118
|
-
webSocket
|
|
118
|
+
webSocket,
|
|
119
|
+
eventSource
|
|
119
120
|
} = {}) {
|
|
120
121
|
if (keepAlive !== undefined) {
|
|
121
122
|
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
|
|
@@ -223,7 +224,7 @@ class Client extends DispatcherBase {
|
|
|
223
224
|
throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0')
|
|
224
225
|
}
|
|
225
226
|
|
|
226
|
-
super({ webSocket })
|
|
227
|
+
super({ webSocket, eventSource })
|
|
227
228
|
|
|
228
229
|
if (typeof connect !== 'function') {
|
|
229
230
|
connect = buildConnector({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const buffer = require('node:buffer')
|
|
3
4
|
const Dispatcher = require('./dispatcher')
|
|
4
5
|
const UnwrapHandler = require('../handler/unwrap-handler')
|
|
5
6
|
const {
|
|
@@ -12,6 +13,7 @@ const { kDestroy, kClose, kClosed, kDestroyed, kDispatch } = require('../core/sy
|
|
|
12
13
|
const kOnDestroyed = Symbol('onDestroyed')
|
|
13
14
|
const kOnClosed = Symbol('onClosed')
|
|
14
15
|
const kWebSocketOptions = Symbol('webSocketOptions')
|
|
16
|
+
const kEventSourceOptions = Symbol('eventSourceOptions')
|
|
15
17
|
|
|
16
18
|
class DispatcherBase extends Dispatcher {
|
|
17
19
|
/** @type {boolean} */
|
|
@@ -32,6 +34,7 @@ class DispatcherBase extends Dispatcher {
|
|
|
32
34
|
constructor (opts) {
|
|
33
35
|
super()
|
|
34
36
|
this[kWebSocketOptions] = opts?.webSocket ?? {}
|
|
37
|
+
this[kEventSourceOptions] = opts?.eventSource ?? {}
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
/**
|
|
@@ -44,6 +47,15 @@ class DispatcherBase extends Dispatcher {
|
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
/**
|
|
51
|
+
* @returns {import('../../types/client').Client.EventSourceOptions}
|
|
52
|
+
*/
|
|
53
|
+
get eventSourceOptions () {
|
|
54
|
+
return {
|
|
55
|
+
maxEventSize: this[kEventSourceOptions].maxEventSize ?? buffer.kStringMaxLength
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
47
59
|
/** @returns {boolean} */
|
|
48
60
|
get destroyed () {
|
|
49
61
|
return this[kDestroyed]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const util = require('../core/util')
|
|
4
|
+
const { kRedirectCacheControl } = require('../core/symbols')
|
|
4
5
|
const {
|
|
5
6
|
parseCacheControlHeader,
|
|
6
7
|
hasInvalidCacheControlDirective,
|
|
@@ -13,6 +14,19 @@ const { parseHttpDate } = require('../util/date.js')
|
|
|
13
14
|
|
|
14
15
|
function noop () {}
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* @param {string | URL} a
|
|
19
|
+
* @param {string | URL} b
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
function sameOrigin (a, b) {
|
|
23
|
+
try {
|
|
24
|
+
return new URL(String(a)).origin === new URL(String(b)).origin
|
|
25
|
+
} catch {
|
|
26
|
+
return String(a) === String(b)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
// Status codes that we can use some heuristics on to cache
|
|
17
31
|
const HEURISTICALLY_CACHEABLE_STATUS_CODES = [
|
|
18
32
|
200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501
|
|
@@ -151,6 +165,14 @@ class CacheHandler {
|
|
|
151
165
|
*/
|
|
152
166
|
#writeStream
|
|
153
167
|
|
|
168
|
+
/**
|
|
169
|
+
* The URLs visited while following redirects, shared by reference from the
|
|
170
|
+
* redirect handler through the request context. Carries the suppressed hops'
|
|
171
|
+
* cache-control under {@link kRedirectCacheControl}.
|
|
172
|
+
* @type {URL[] | undefined}
|
|
173
|
+
*/
|
|
174
|
+
#history
|
|
175
|
+
|
|
154
176
|
/**
|
|
155
177
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} opts
|
|
156
178
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
|
|
@@ -167,6 +189,9 @@ class CacheHandler {
|
|
|
167
189
|
onRequestStart (controller, context) {
|
|
168
190
|
this.#writeStream?.destroy()
|
|
169
191
|
this.#writeStream = undefined
|
|
192
|
+
// The redirect handler mutates this shared history as redirects are
|
|
193
|
+
// followed, allowing the cache to identify the response's actual origin.
|
|
194
|
+
this.#history = context?.history
|
|
170
195
|
this.#handler.onRequestStart?.(controller, context)
|
|
171
196
|
}
|
|
172
197
|
|
|
@@ -206,6 +231,29 @@ class CacheHandler {
|
|
|
206
231
|
return downstreamOnHeaders()
|
|
207
232
|
}
|
|
208
233
|
|
|
234
|
+
// Never store a response produced by a different origin under the original
|
|
235
|
+
// request's cache key.
|
|
236
|
+
if (this.#history && this.#history.length > 0) {
|
|
237
|
+
const finalUrl = this.#history[this.#history.length - 1]
|
|
238
|
+
if (!sameOrigin(finalUrl.origin, this.#cacheKey.origin)) {
|
|
239
|
+
return downstreamOnHeaders()
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Preserve restrictive cache policy from suppressed redirect responses.
|
|
244
|
+
const redirectCacheControl = this.#history?.[kRedirectCacheControl]
|
|
245
|
+
if (redirectCacheControl) {
|
|
246
|
+
for (const hopCacheControl of redirectCacheControl) {
|
|
247
|
+
const hopDirectives = hopCacheControl ? parseCacheControlHeader(hopCacheControl) : {}
|
|
248
|
+
if (
|
|
249
|
+
hopDirectives['no-store'] === true ||
|
|
250
|
+
(this.#cacheType === 'shared' && hopDirectives.private === true)
|
|
251
|
+
) {
|
|
252
|
+
return downstreamOnHeaders()
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
209
257
|
const cacheControlHeader = resHeaders['cache-control']
|
|
210
258
|
const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
|
|
211
259
|
|
|
@@ -16,6 +16,7 @@ const DEFAULT_MAX_BUFFER_SIZE = 5 * 1024 * 1024
|
|
|
16
16
|
* @property {number} bufferedBytes
|
|
17
17
|
* @property {object | null} pendingTrailers
|
|
18
18
|
* @property {boolean} done
|
|
19
|
+
* @property {boolean} [pendingStart] onRequestStart deferred until the primary request's context is known
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -30,6 +31,20 @@ class DeduplicationHandler {
|
|
|
30
31
|
*/
|
|
31
32
|
#primaryHandler
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Whether the primary request has started (onRequestStart received). Until
|
|
36
|
+
* then the request context is unknown and cannot be given to waiting handlers.
|
|
37
|
+
* @type {boolean}
|
|
38
|
+
*/
|
|
39
|
+
#requestStarted = false
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The primary request context, replayed to waiting handlers so they observe
|
|
43
|
+
* the same redirect history as the primary.
|
|
44
|
+
* @type {any}
|
|
45
|
+
*/
|
|
46
|
+
#context = null
|
|
47
|
+
|
|
33
48
|
/**
|
|
34
49
|
* @type {WaitingHandler[]}
|
|
35
50
|
*/
|
|
@@ -112,20 +127,30 @@ class DeduplicationHandler {
|
|
|
112
127
|
const waitingController = waitingHandler.controller
|
|
113
128
|
|
|
114
129
|
try {
|
|
115
|
-
|
|
130
|
+
if (this.#requestStarted) {
|
|
131
|
+
// The primary request has started, so its context (including the
|
|
132
|
+
// redirect history) is known and can be delivered immediately.
|
|
133
|
+
handler.onRequestStart?.(waitingController, this.#context)
|
|
116
134
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
135
|
+
if (waitingController.aborted) {
|
|
136
|
+
waitingHandler.done = true
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
121
139
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
140
|
+
if (this.#responseStarted) {
|
|
141
|
+
handler.onResponseStart?.(
|
|
142
|
+
waitingController,
|
|
143
|
+
this.#statusCode,
|
|
144
|
+
this.#headers,
|
|
145
|
+
this.#statusMessage
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
// The primary request has not started yet. Defer onRequestStart until
|
|
150
|
+
// its context is known, so the waiting handler observes the same
|
|
151
|
+
// redirect history as the primary (a cache handler relies on it to
|
|
152
|
+
// reject cross-origin redirect responses).
|
|
153
|
+
waitingHandler.pendingStart = true
|
|
129
154
|
}
|
|
130
155
|
} catch {
|
|
131
156
|
// Ignore errors from waiting handlers
|
|
@@ -146,6 +171,24 @@ class DeduplicationHandler {
|
|
|
146
171
|
*/
|
|
147
172
|
onRequestStart (controller, context) {
|
|
148
173
|
this.#controller = controller
|
|
174
|
+
if (!this.#requestStarted) {
|
|
175
|
+
this.#requestStarted = true
|
|
176
|
+
this.#context = context
|
|
177
|
+
// Deliver the now-known context to handlers that joined before the
|
|
178
|
+
// primary request started.
|
|
179
|
+
for (const waitingHandler of this.#waitingHandlers) {
|
|
180
|
+
if (waitingHandler.pendingStart && !waitingHandler.done && !waitingHandler.controller.aborted) {
|
|
181
|
+
waitingHandler.pendingStart = false
|
|
182
|
+
try {
|
|
183
|
+
waitingHandler.handler.onRequestStart?.(waitingHandler.controller, context)
|
|
184
|
+
} catch {
|
|
185
|
+
// Ignore errors from waiting handlers
|
|
186
|
+
waitingHandler.done = true
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
this.#pruneDoneWaitingHandlers()
|
|
191
|
+
}
|
|
149
192
|
this.#primaryHandler.onRequestStart?.(controller, context)
|
|
150
193
|
}
|
|
151
194
|
|
|
@@ -319,7 +362,8 @@ class DeduplicationHandler {
|
|
|
319
362
|
bufferedChunks: [],
|
|
320
363
|
bufferedBytes: 0,
|
|
321
364
|
pendingTrailers: null,
|
|
322
|
-
done: false
|
|
365
|
+
done: false,
|
|
366
|
+
pendingStart: false
|
|
323
367
|
}
|
|
324
368
|
|
|
325
369
|
const state = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const util = require('../core/util')
|
|
4
|
-
const { kBodyUsed } = require('../core/symbols')
|
|
4
|
+
const { kBodyUsed, kRedirectCacheControl } = require('../core/symbols')
|
|
5
5
|
const assert = require('node:assert')
|
|
6
6
|
const { InvalidArgumentError } = require('../core/errors')
|
|
7
7
|
const EE = require('node:events')
|
|
@@ -47,6 +47,11 @@ class RedirectHandler {
|
|
|
47
47
|
this.maxRedirections = maxRedirections
|
|
48
48
|
this.handler = handler
|
|
49
49
|
this.history = []
|
|
50
|
+
// Retain cache-control from suppressed redirect hops for an outer cache.
|
|
51
|
+
Object.defineProperty(this.history, kRedirectCacheControl, {
|
|
52
|
+
value: [],
|
|
53
|
+
enumerable: false
|
|
54
|
+
})
|
|
50
55
|
|
|
51
56
|
if (util.isStream(this.opts.body)) {
|
|
52
57
|
// TODO (fix): Provide some way for the user to cache the file to e.g. /tmp
|
|
@@ -130,6 +135,8 @@ class RedirectHandler {
|
|
|
130
135
|
return
|
|
131
136
|
}
|
|
132
137
|
|
|
138
|
+
this.history[kRedirectCacheControl].push(headers['cache-control'])
|
|
139
|
+
|
|
133
140
|
const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)))
|
|
134
141
|
const path = search ? `${pathname}${search}` : pathname
|
|
135
142
|
|
|
@@ -220,9 +220,14 @@ class DecompressHandler extends DecoratorHandler {
|
|
|
220
220
|
this.#decompressedSize = decompressedSize
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
this.#controller.
|
|
223
|
+
try {
|
|
224
|
+
const result = super.onResponseData(this.#controller, chunk)
|
|
225
|
+
if (result === false && !this.#controller.paused) {
|
|
226
|
+
this.#controller.pause()
|
|
227
|
+
}
|
|
228
|
+
} catch (err) {
|
|
229
|
+
this.#fail(err)
|
|
230
|
+
return
|
|
226
231
|
}
|
|
227
232
|
}
|
|
228
233
|
} while (this.#drainRequested && !this.#terminated && !this.#controller.paused)
|
|
@@ -239,9 +244,14 @@ class DecompressHandler extends DecoratorHandler {
|
|
|
239
244
|
return
|
|
240
245
|
}
|
|
241
246
|
|
|
242
|
-
this.#terminated = true
|
|
243
247
|
this.#cleanupDecompressors()
|
|
244
|
-
|
|
248
|
+
try {
|
|
249
|
+
super.onResponseEnd(this.#controller, this.#trailers)
|
|
250
|
+
} catch (err) {
|
|
251
|
+
this.#fail(err)
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
this.#terminated = true
|
|
245
255
|
}
|
|
246
256
|
|
|
247
257
|
#onDecompressionEnd () {
|
|
@@ -32,8 +32,10 @@ function getCookies (headers) {
|
|
|
32
32
|
|
|
33
33
|
const cookie = headers.get('cookie')
|
|
34
34
|
|
|
35
|
+
// A null prototype keeps a cookie named `__proto__` from hitting the
|
|
36
|
+
// Object.prototype setter, which would silently drop it.
|
|
35
37
|
/** @type {Record<string, string>} */
|
|
36
|
-
const out = {}
|
|
38
|
+
const out = { __proto__: null }
|
|
37
39
|
|
|
38
40
|
if (!cookie) {
|
|
39
41
|
return out
|