tunnelfetch 1.9.0 → 1.11.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/README.md CHANGED
@@ -470,18 +470,45 @@ extension **set** that is curl's. A test reads that gap straight out of the comm
470
470
  capture and fails if it changes, so it cannot drift quietly — but it is a real difference and a
471
471
  JA3/JA4 hash sees it.
472
472
 
473
+ `tls.omitExtensions` is the subtractive counterpart, and `status_request` (5) is what it exists for:
474
+ that is the one extension this package sends which curl does not, so an identity matching a sample
475
+ without it had no way to drop it. Dropping it gives up OCSP stapling — the only revocation signal
476
+ this package consumes — so pairing it with `trust.revocation: 'require-staple'` is refused at
477
+ configuration time rather than left to fail every connection on a certificate that was never asked
478
+ to carry a staple.
479
+
473
480
  `tls.extraExtensions` takes pre-encoded extensions and is the only way to close it. They are ordered
474
481
  like any other and reproduced on a HelloRetryRequest retry, because a second hello that changed its
475
482
  extension set would be both malformed (RFC 8446 §4.1.2) and a signal in itself. Encoding them
476
483
  correctly is the caller's job; this package does not parse what it did not build.
477
484
 
478
- **Every offered cipher must be one this package can perform.** `tls.ciphers` used to be taken
479
- verbatim, so a list containing a CBC or RSA-key-exchange suite put a number on the wire that a
480
- server could select after which the AEAD layer had nothing to build and the connection died
481
- mid-handshake. Such a list is now refused at configuration time. An explicit `TLS_CHACHA20_POLY1305_SHA256`
482
- without an injected implementation is refused too, rather than silently dropped: quietly presenting
483
- a different fingerprint from the one you asked for is the worst outcome available to a package like
484
- this one.
485
+ **Every offered cipher must be one this package can perform unless you say otherwise.**
486
+ `tls.ciphers` used to be taken verbatim, so a list containing a CBC or RSA-key-exchange suite put a
487
+ number on the wire that a server could select, after which the AEAD layer had nothing to build and
488
+ the connection died mid-handshake. Such a list is refused at configuration time by default, and an
489
+ explicit `TLS_CHACHA20_POLY1305_SHA256` with no injected implementation is refused rather than
490
+ silently dropped — quietly presenting a different fingerprint from the one you asked for is the
491
+ worst outcome available to a package like this one.
492
+
493
+ But refusing outright is the wrong default to have no escape from, because **the restriction is
494
+ itself a fingerprint**:
495
+
496
+ | | suites offered | performable here |
497
+ | --- | --- | --- |
498
+ | curl 8.21.0 | 30 | 7 |
499
+ | Chromium | 15 | 7 |
500
+
501
+ A hello restricted to what can be honoured carries a cipher list less than half the length of any
502
+ real client's, and list length and contents are exactly what a JA3 hash reads. `tls.allowUnperformableCiphers`
503
+ offers the accurate list. The trade is narrow: the first unperformable suite sits at index 5 of
504
+ curl's list and 7 of Chromium's, behind the TLS 1.3 suites, so a server with 1.3 available never
505
+ reaches one. If a server does select one the handshake fails, and the error names the option rather
506
+ than reading like a defect here.
507
+
508
+ Most of the gap is not a missing feature. Sixteen of curl's twenty-three are CBC — MAC-then-encrypt,
509
+ which cannot be implemented without a Lucky13 padding oracle in JavaScript — and two more are RSA key
510
+ exchange with no forward secrecy. Both are refusals this package intends to keep. The three that
511
+ *are* implementable are the TLS 1.2 ChaCha20-Poly1305 suites, and they are not implemented yet.
485
512
 
486
513
  Extension order matters because JA3 and JA4 hash the extension list **in wire order**, so it is most
487
514
  of what a fingerprinter reads. `pre_shared_key` is forced last whatever you ask for: RFC 8446
@@ -578,6 +605,20 @@ new Client({ connect, proxy, maxBodyBytes: Infinity }); // or any number you h
578
605
  The trade is deliberate: an unasked-for limit is discoverable the first time it bites, and names the
579
606
  option in its error. An unasked-for OOM is neither.
580
607
 
608
+ #### The proxy sees a fingerprint too
609
+
610
+ `Proxy-Connection` is a pre-standard hop header that never reached a spec. The origin never sees it;
611
+ the proxy always does. Clients disagree — some send `keep-alive`, some `close`, some omit it — so for
612
+ anyone matching a client's behaviour *at the proxy* it is part of the fingerprint, and it used to be
613
+ hard-coded.
614
+
615
+ ```js
616
+ new Client({ connect, proxy: { ...cfg, proxyConnection: 'close' } }); // or null to omit it
617
+ ```
618
+
619
+ The default stays `keep-alive`, which avoids a class of proxy that closes the tunnel after one
620
+ request. Omitting the header is not the same as sending `close`.
621
+
581
622
  ### Trust — the `verify=` knob
582
623
 
583
624
  ```js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tunnelfetch",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "A fetch-shaped HTTP client that can route through HTTP CONNECT / HTTPS / SOCKS5 proxies on runtimes with only raw TCP, such as Cloudflare Workers. Implements TLS in userland because the runtime cannot verify a tunnelled peer.",
5
5
  "keywords": [
6
6
  "fetch",
@@ -122,9 +122,15 @@ function buildConnectRequest(proxy, target) {
122
122
  if (proxy.username) {
123
123
  lines.push(`Proxy-Authorization: Basic ${basicCredentials(proxy.username, proxy.password)}`);
124
124
  }
125
- // Some proxies still key off the pre-standard hop header; sending it is harmless and avoids a
126
- // class of proxy that closes the tunnel after one request without it.
127
- lines.push('Proxy-Connection: keep-alive');
125
+ // `Proxy-Connection` is a pre-standard hop header that never made it into a spec, and clients
126
+ // disagree about it: some send `keep-alive`, some send `close`, some omit it. The proxy sees this
127
+ // even though the origin never does, so for anyone matching a client's behaviour AT THE PROXY it
128
+ // is part of the fingerprint. `keep-alive` stays the default — it avoids a class of proxy that
129
+ // closes the tunnel after one request — but it is no longer fixed.
130
+ //
131
+ // `null` omits the header entirely, which is not the same as sending `close`.
132
+ const pc = proxy.proxyConnection === undefined ? 'keep-alive' : proxy.proxyConnection;
133
+ if (pc !== null) lines.push(`Proxy-Connection: ${pc}`);
128
134
  return `${lines.join('\r\n')}\r\n\r\n`;
129
135
  }
130
136
 
@@ -19,7 +19,13 @@ import { openSocks5 } from './socks5.js';
19
19
  * @typedef {(addr: {hostname: string, port: number},
20
20
  * opts?: {secureTransport?: 'off'|'on'|'starttls', allowHalfOpen?: boolean}) => Duplex} ConnectFn
21
21
  * @typedef {{ protocol: 'http'|'https'|'socks5'|'socks5h', hostname: string, port: number,
22
- * username?: string, password?: string }} ProxyConfig
22
+ * username?: string, password?: string,
23
+ * proxyConnection?: string | null }} ProxyConfig
24
+ *
25
+ * `proxyConnection` sets the pre-standard `Proxy-Connection` header on a CONNECT request, or
26
+ * omits it entirely when null. Default 'keep-alive'. The origin never sees this header; the
27
+ * proxy does, so it belongs to whatever fingerprint the proxy is reading. Clients disagree —
28
+ * some send keep-alive, some close, some nothing — and omitting is not the same as 'close'.
23
29
  */
24
30
 
25
31
  const DEFAULT_PORTS = { http: 8080, https: 443, socks5: 1080, socks5h: 1080 };
@@ -114,6 +120,10 @@ function normalise(cfg) {
114
120
  port,
115
121
  username: cfg.username || undefined,
116
122
  password: cfg.password || undefined,
123
+ // Listed explicitly because this function REBUILDS the config rather than copying it, so a
124
+ // field not named here is dropped without a word. That is how `http2ConnectionWindow` came to
125
+ // be declared in a profile and read by nothing.
126
+ proxyConnection: cfg.proxyConnection,
117
127
  });
118
128
  }
119
129
 
@@ -137,9 +137,23 @@ function expectServerHello(msg, offers12) {
137
137
  * @property {number[]} [groups] supported_groups, in preference order.
138
138
  * @property {number[]} [offerGroups] groups to send an actual key_share for. Default the first
139
139
  * supported group; a HelloRetryRequest recovers any other choice at the cost of a round trip.
140
- * @property {number[]} [ciphers] cipher suites to offer, in preference order. Every suite must be
141
- * one this package can perform; an offer it cannot honour is a dead connection the moment a
142
- * server selects it, so an unknown suite is refused here rather than on the wire.
140
+ * @property {number[]} [ciphers] cipher suites to offer, in preference order. By default every
141
+ * suite must be one this package can perform: an offer it cannot honour is a dead connection the
142
+ * moment a server selects it, so an unknown suite is refused here rather than on the wire.
143
+ * @property {number[]} [omitExtensions] extension types to leave out of the ClientHello, the
144
+ * subtractive counterpart to `extraExtensions`. `status_request` (5) is the one extension this
145
+ * package sends that curl does not, so an identity matching a sample without it needs this.
146
+ * Dropping it gives up OCSP stapling, which is the only revocation signal this package can
147
+ * consume — pairing it with `trust.revocation: 'require-staple'` is refused rather than left to
148
+ * fail every connection.
149
+ * @property {boolean} [allowUnperformableCiphers] offer suites this package cannot complete.
150
+ * For fingerprint fidelity only. Real clients offer far more than this package implements — curl
151
+ * 8.21.0 offers thirty against seven performable here, Chromium fifteen against seven — so a
152
+ * hello restricted to what it can honour carries a cipher list shorter than any real client's,
153
+ * which is exactly what a JA3 hash reads. With this set, a server that selects an unperformable
154
+ * suite fails the handshake; the first such suite sits behind the TLS 1.3 ones in both real
155
+ * lists, so a 1.3-capable server does not reach it. Knowingly trading a rare failure for an
156
+ * accurate fingerprint is a legitimate choice; making it silently is not.
143
157
  * @property {Uint8Array[]} [extraExtensions] pre-encoded ClientHello extensions, appended before
144
158
  * ordering. `extensionOrder` can only arrange extensions that were BUILT — it filters to what
145
159
  * exists and sorts that — so ordering alone cannot produce an extension this package does not
@@ -347,7 +361,16 @@ async function drive({ record, hostname, verifyPeer, options, deps, versions })
347
361
  // package cannot perform is an offer a server may take and then find unhonoured, which fails the
348
362
  // connection rather than merely looking wrong". It applies just as much to `tls.ciphers`, and
349
363
  // now does. CIPHER_PARAMS is the set this package knows how to key and seal.
350
- if (options.ciphers) {
364
+ //
365
+ // `allowUnperformableCiphers` opts out, and it exists because refusing outright was the wrong
366
+ // default to have no escape from. Every real client offers far more suites than this package
367
+ // implements — curl 8.21.0 offers thirty and seven are performable here; Chromium offers fifteen
368
+ // and seven are — so a hello restricted to what can be honoured has a cipher list shorter than
369
+ // any real client's, and that is itself what a JA3 hash reads. A caller matching a fingerprint
370
+ // may rationally prefer the accurate list: the first unperformable suite sits at index 5 of
371
+ // curl's and 7 of Chromium's, behind the TLS 1.3 suites, so a server with 1.3 available never
372
+ // reaches it. The risk is real but narrow, and it is the caller's to take knowingly.
373
+ if (options.ciphers && !options.allowUnperformableCiphers) {
351
374
  const unperformable = ciphers.filter((c) => !CIPHER_PARAMS[c]);
352
375
  if (unperformable.length) {
353
376
  throw new TlsError(
@@ -414,6 +437,7 @@ async function drive({ record, hostname, verifyPeer, options, deps, versions })
414
437
  alpn,
415
438
  ciphers,
416
439
  extraExtensions: options.extraExtensions ?? [],
440
+ omitExtensions: options.omitExtensions ?? [],
417
441
  versions,
418
442
  extensionOrder: options.extensionOrder,
419
443
  sigSchemes: options.sigSchemes,
@@ -192,6 +192,7 @@ export async function deriveSharedSecret(group, privateKey, peerKey, deps = {})
192
192
  * @property {string[]} [alpn] default ['http/1.1']; empty array omits the extension
193
193
  * @property {number[]} [versions] default [TLS13, TLS12]
194
194
  * @property {Uint8Array[]} [extraExtensions] pre-encoded, sent verbatim (the HRR cookie)
195
+ * @property {number[]} [omitExtensions] extension types to leave out of the hello
195
196
  * @property {{ identity: Uint8Array, obfuscatedTicketAge: number, binderLen: number }} [psk]
196
197
  * offer this resumption PSK. Encoded with a zeroed binder placeholder; the caller MUST derive
197
198
  * the real binder over `message.subarray(0, truncatedLength)` and patch it in at
@@ -299,6 +300,7 @@ export function buildClientHello({
299
300
  versions = [TLS13, TLS12],
300
301
  extensionOrder = CURL_EXTENSION_ORDER,
301
302
  extraExtensions = [],
303
+ omitExtensions = [],
302
304
  psk = null,
303
305
  grease = false,
304
306
  randomBytes = defaultRandom,
@@ -341,11 +343,15 @@ export function buildClientHello({
341
343
  'a resumption PSK was supplied but TLS 1.3 is not among the offered versions');
342
344
  }
343
345
 
346
+ const omit = new Set(omitExtensions);
344
347
  const extensionParts = [
345
348
  encodeServerName(hostname),
346
- // Always offered, for either version: without it a server may not staple (RFC 6066 s8), and
347
- // a stapled OCSP response is the only revocation signal this package can consume.
348
- encodeStatusRequest(),
349
+ // Offered by default for either version: without it a server may not staple (RFC 6066 s8), and
350
+ // a stapled OCSP response is the only revocation signal this package can consume. It is also
351
+ // the one extension this package sends that curl does not, so an identity matching a sample
352
+ // without it needs a way to drop it — `omitExtensions` is the subtractive counterpart to
353
+ // `extraExtensions`, and dropping this one gives up stapling.
354
+ omit.has(EXTENSION.status_request) ? null : encodeStatusRequest(),
349
355
  encodeSupportedGroups(g ? [g.take(), ...groups] : groups),
350
356
  encodeSignatureAlgorithms(sigSchemes),
351
357
  alpn.length ? encodeAlpn(alpn) : null,
@@ -589,7 +595,10 @@ export function negotiateCipher(serverHello, { offeredCiphers, version }) {
589
595
  if (!params) {
590
596
  throw new TlsUnsupportedError(
591
597
  codes.TLS_CIPHER_UNSUPPORTED,
592
- `cipher suite ${hex16(suite)} has no parameters; this is a bug in the offer list`,
598
+ `cipher suite ${hex16(suite)} was offered but this package cannot perform it, so the ` +
599
+ 'handshake cannot continue. Either the offer list has a bug, or `allowUnperformableCiphers` ' +
600
+ 'was set to match a real client\'s cipher list and a server has selected one of the suites ' +
601
+ 'that choice knowingly put on the wire.',
593
602
  { cipherSuite: suite },
594
603
  );
595
604
  }
@@ -250,6 +250,7 @@ export async function continueTls13(ctx) {
250
250
  // not permit a second hello to change its extension SET, and a retry that quietly dropped
251
251
  // them would present one fingerprint on the first flight and a different one on the second —
252
252
  // a difference that is itself a signal.
253
+ omitExtensions: options.omitExtensions ?? [],
253
254
  extraExtensions: [
254
255
  ...(options.extraExtensions ?? []),
255
256
  ...(cookie ? [cookieExtension(cookie)] : []),
package/src/transport.js CHANGED
@@ -124,6 +124,20 @@ export async function openConnection({
124
124
  }) {
125
125
  const target = targetFromUrl(url);
126
126
  const proxyConfig = parseProxy(proxy);
127
+
128
+ // Dropping status_request gives up OCSP stapling, and stapling is the only revocation signal this
129
+ // package consumes. Combined with `require-staple` that is not a weakened check, it is a policy
130
+ // that can never be satisfied: every connection would fail with OCSP_REQUIRED, on a certificate
131
+ // that was never asked to carry a staple. Refused here, where both settings are visible.
132
+ if ((tls?.omitExtensions ?? []).includes(5) && trust?.revocation === 'require-staple') {
133
+ throw new ConfigError(
134
+ codes.CONFIG_INVALID,
135
+ "tls.omitExtensions drops status_request (5) while trust.revocation is 'require-staple'. " +
136
+ 'Without status_request a server is not asked to staple, so no staple can arrive and every ' +
137
+ 'connection would fail. Keep the extension, or relax the revocation policy — the two are ' +
138
+ 'a contradiction rather than a stricter setting.',
139
+ );
140
+ }
127
141
  const owns = !deadlines;
128
142
  const dl = deadlines ?? new DeadlineController({}, { signal });
129
143
 
@@ -49,12 +49,19 @@ export type ConnectFn = (addr: {
49
49
  secureTransport?: "off" | "on" | "starttls";
50
50
  allowHalfOpen?: boolean;
51
51
  }) => Duplex;
52
+ /**
53
+ * `proxyConnection` sets the pre-standard `Proxy-Connection` header on a CONNECT request, or
54
+ * omits it entirely when null. Default 'keep-alive'. The origin never sees this header; the
55
+ * proxy does, so it belongs to whatever fingerprint the proxy is reading. Clients disagree —
56
+ * some send keep-alive, some close, some nothing — and omitting is not the same as 'close'.
57
+ */
52
58
  export type ProxyConfig = {
53
59
  protocol: "http" | "https" | "socks5" | "socks5h";
54
60
  hostname: string;
55
61
  port: number;
56
62
  username?: string;
57
63
  password?: string;
64
+ proxyConnection?: string | null;
58
65
  };
59
66
  import { openDirect } from './direct.js';
60
67
  import { openHttpConnect } from './http-connect.js';
@@ -13,9 +13,23 @@
13
13
  * @property {number[]} [groups] supported_groups, in preference order.
14
14
  * @property {number[]} [offerGroups] groups to send an actual key_share for. Default the first
15
15
  * supported group; a HelloRetryRequest recovers any other choice at the cost of a round trip.
16
- * @property {number[]} [ciphers] cipher suites to offer, in preference order. Every suite must be
17
- * one this package can perform; an offer it cannot honour is a dead connection the moment a
18
- * server selects it, so an unknown suite is refused here rather than on the wire.
16
+ * @property {number[]} [ciphers] cipher suites to offer, in preference order. By default every
17
+ * suite must be one this package can perform: an offer it cannot honour is a dead connection the
18
+ * moment a server selects it, so an unknown suite is refused here rather than on the wire.
19
+ * @property {number[]} [omitExtensions] extension types to leave out of the ClientHello, the
20
+ * subtractive counterpart to `extraExtensions`. `status_request` (5) is the one extension this
21
+ * package sends that curl does not, so an identity matching a sample without it needs this.
22
+ * Dropping it gives up OCSP stapling, which is the only revocation signal this package can
23
+ * consume — pairing it with `trust.revocation: 'require-staple'` is refused rather than left to
24
+ * fail every connection.
25
+ * @property {boolean} [allowUnperformableCiphers] offer suites this package cannot complete.
26
+ * For fingerprint fidelity only. Real clients offer far more than this package implements — curl
27
+ * 8.21.0 offers thirty against seven performable here, Chromium fifteen against seven — so a
28
+ * hello restricted to what it can honour carries a cipher list shorter than any real client's,
29
+ * which is exactly what a JA3 hash reads. With this set, a server that selects an unperformable
30
+ * suite fails the handshake; the first such suite sits behind the TLS 1.3 ones in both real
31
+ * lists, so a 1.3-capable server does not reach it. Knowingly trading a rare failure for an
32
+ * accurate fingerprint is a legitimate choice; making it silently is not.
19
33
  * @property {Uint8Array[]} [extraExtensions] pre-encoded ClientHello extensions, appended before
20
34
  * ordering. `extensionOrder` can only arrange extensions that were BUILT — it filters to what
21
35
  * exists and sorts that — so ordering alone cannot produce an extension this package does not
@@ -169,11 +183,31 @@ export type TlsOptions = {
169
183
  */
170
184
  offerGroups?: number[] | undefined;
171
185
  /**
172
- * cipher suites to offer, in preference order. Every suite must be
173
- * one this package can perform; an offer it cannot honour is a dead connection the moment a
174
- * server selects it, so an unknown suite is refused here rather than on the wire.
186
+ * cipher suites to offer, in preference order. By default every
187
+ * suite must be one this package can perform: an offer it cannot honour is a dead connection the
188
+ * moment a server selects it, so an unknown suite is refused here rather than on the wire.
175
189
  */
176
190
  ciphers?: number[] | undefined;
191
+ /**
192
+ * extension types to leave out of the ClientHello, the
193
+ * subtractive counterpart to `extraExtensions`. `status_request` (5) is the one extension this
194
+ * package sends that curl does not, so an identity matching a sample without it needs this.
195
+ * Dropping it gives up OCSP stapling, which is the only revocation signal this package can
196
+ * consume — pairing it with `trust.revocation: 'require-staple'` is refused rather than left to
197
+ * fail every connection.
198
+ */
199
+ omitExtensions?: number[] | undefined;
200
+ /**
201
+ * offer suites this package cannot complete.
202
+ * For fingerprint fidelity only. Real clients offer far more than this package implements — curl
203
+ * 8.21.0 offers thirty against seven performable here, Chromium fifteen against seven — so a
204
+ * hello restricted to what it can honour carries a cipher list shorter than any real client's,
205
+ * which is exactly what a JA3 hash reads. With this set, a server that selects an unperformable
206
+ * suite fails the handshake; the first such suite sits behind the TLS 1.3 ones in both real
207
+ * lists, so a 1.3-capable server does not reach it. Knowingly trading a rare failure for an
208
+ * accurate fingerprint is a legitimate choice; making it silently is not.
209
+ */
210
+ allowUnperformableCiphers?: boolean | undefined;
177
211
  /**
178
212
  * pre-encoded ClientHello extensions, appended before
179
213
  * ordering. `extensionOrder` can only arrange extensions that were BUILT — it filters to what
@@ -32,7 +32,7 @@ export function generateKeyShare(group: number, deps?: import("./connect.js").Tl
32
32
  * @returns {Promise<Uint8Array>} throws on any degenerate or malformed peer key
33
33
  */
34
34
  export function deriveSharedSecret(group: number, privateKey: CryptoKey | import("./hybrid.js").HybridPrivate, peerKey: Uint8Array, deps?: import("./connect.js").TlsDeps): Promise<Uint8Array>;
35
- export function buildClientHello({ hostname, keyShares, random, legacySessionId, ciphers, groups, sigSchemes, alpn, versions, extensionOrder, extraExtensions, psk, grease, randomBytes, }: {
35
+ export function buildClientHello({ hostname, keyShares, random, legacySessionId, ciphers, groups, sigSchemes, alpn, versions, extensionOrder, extraExtensions, omitExtensions, psk, grease, randomBytes, }: {
36
36
  hostname: any;
37
37
  keyShares: any;
38
38
  random: any;
@@ -44,6 +44,7 @@ export function buildClientHello({ hostname, keyShares, random, legacySessionId,
44
44
  versions?: number[] | undefined;
45
45
  extensionOrder?: readonly number[] | undefined;
46
46
  extraExtensions?: never[] | undefined;
47
+ omitExtensions?: never[] | undefined;
47
48
  psk?: null | undefined;
48
49
  grease?: boolean | undefined;
49
50
  randomBytes?: ((n: any) => Uint8Array<any>) | undefined;
@@ -305,6 +306,7 @@ export function checkAlpn(extensions: Map<number, Uint8Array>, offeredAlpn: stri
305
306
  * @property {string[]} [alpn] default ['http/1.1']; empty array omits the extension
306
307
  * @property {number[]} [versions] default [TLS13, TLS12]
307
308
  * @property {Uint8Array[]} [extraExtensions] pre-encoded, sent verbatim (the HRR cookie)
309
+ * @property {number[]} [omitExtensions] extension types to leave out of the hello
308
310
  * @property {{ identity: Uint8Array, obfuscatedTicketAge: number, binderLen: number }} [psk]
309
311
  * offer this resumption PSK. Encoded with a zeroed binder placeholder; the caller MUST derive
310
312
  * the real binder over `message.subarray(0, truncatedLength)` and patch it in at
@@ -432,6 +434,10 @@ export type ClientHelloOptions = {
432
434
  * pre-encoded, sent verbatim (the HRR cookie)
433
435
  */
434
436
  extraExtensions?: Uint8Array<ArrayBufferLike>[] | undefined;
437
+ /**
438
+ * extension types to leave out of the hello
439
+ */
440
+ omitExtensions?: number[] | undefined;
435
441
  /**
436
442
  * offer this resumption PSK. Encoded with a zeroed binder placeholder; the caller MUST derive
437
443
  * the real binder over `message.subarray(0, truncatedLength)` and patch it in at