tunnelfetch 1.1.2 → 1.2.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
@@ -311,10 +311,53 @@ behaviour — measure your own targets, and expect the answer to change. Our TLS
311
311
  identical outcomes on every reachable host in that sample, so JA3-style TLS shaping is not what
312
312
  gates access here — but curl's **HTTP/2** fingerprint passed where HTTP/1.1 was challenged. So the
313
313
  `SETTINGS` frame values, the initial window sizes, the connection `WINDOW_UPDATE`, and the
314
- pseudo-header order are matched byte-for-byte to curl (8.7.1 / nghttp2), captured off the wire.
314
+ pseudo-header order are matched byte-for-byte to curl (nghttp2 1.69.0), captured off the wire.
315
315
  This is empirical: a naïve h2 fingerprint can fail exactly where curl's succeeds, which would waste
316
316
  the whole exercise.
317
317
 
318
+ ### Fingerprints
319
+
320
+ Both halves are matched to **curl 8.21.0 / OpenSSL 3.6.3** and both are fully configurable. The TLS
321
+ backend matters more than the curl version — the same curl built against SecureTransport produces a
322
+ completely different ClientHello — so the reference is captured off the wire, not recalled, and
323
+ pinned in `test/tls/fingerprint.test.js` and `test/http2/fingerprint.test.js`.
324
+
325
+ | Layer | Default | Configure with |
326
+ |---|---|---|
327
+ | ClientHello extension **order** | curl's, exactly, for every extension both send | `tls.extensionOrder` |
328
+ | Cipher suites | the AEAD suites this package implements, in curl's relative order | `tls.ciphers` |
329
+ | Supported groups | `x25519, secp256r1, secp384r1, secp521r1` | `tls.groups` |
330
+ | Signature algorithms | ECDSA and RSA-PSS/PKCS#1 over SHA-256/384/512 | `tls.sigSchemes` |
331
+ | ALPN | `h2, http/1.1` | `tls.alpn` |
332
+ | HTTP/2 `SETTINGS` ids **and order** | curl's: `MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, ENABLE_PUSH` | `http2Settings` |
333
+ | h2 preface, `WINDOW_UPDATE`, pseudo-header order, HPACK representation | curl's, byte-for-byte | fixed |
334
+ | `Accept-Encoding` | `gzip, deflate` — curl's | `decoders` appends |
335
+
336
+ Extension order matters because JA3 and JA4 hash the extension list **in wire order**, so it is most
337
+ of what a fingerprinter reads. `pre_shared_key` is forced last whatever you ask for: RFC 8446
338
+ §4.2.11 defines the binder transcript as the hello truncated just before the binders, which is a
339
+ well-defined byte range only if nothing follows them.
340
+
341
+ **Where the default deliberately differs from curl**, and why it cannot simply be copied: a
342
+ ClientHello is an *offer*, and a server may take you up on any of it. Advertising what you cannot do
343
+ trades a fingerprint mismatch for a broken handshake, which is worse and fails silently.
344
+
345
+ | curl sends | This package | Why |
346
+ |---|---|---|
347
+ | 30 cipher suites, incl. RSA key exchange and CBC | 6 AEAD suites | Not implemented, by design. A server selecting `TLS_RSA_WITH_AES_256_CBC_SHA` would get a dead connection |
348
+ | `X25519MLKEM768` group and a 1216-byte key share | not offered | ML-KEM is not implemented |
349
+ | SHA-1 signature schemes | not offered | Refused deliberately |
350
+ | `encrypt_then_mac` | not sent | Applies only to CBC suites, which are not offered |
351
+ | `post_handshake_auth` | not sent | Invites a post-handshake `CertificateRequest`, which is not implemented |
352
+ | — | `status_request` | curl does not ask for a stapled OCSP response; this package must, because a staple is its only revocation signal |
353
+
354
+ Closing that gap means implementing ML-KEM, RSA key exchange and CBC suites — a different project,
355
+ and the last two are things this package refuses on purpose. `tls.ciphers` and `tls.groups` will let
356
+ you offer them anyway; the handshake will then fail if a server picks one, and that is yours to own.
357
+
358
+ A test asserts this delta is exactly the list above, so gaining one of these capabilities without
359
+ updating the table fails the build.
360
+
318
361
  Everything an HTTP/1.1 body has, an HTTP/2 body keeps: streaming (SSE works unchanged), trailers,
319
362
  gzip decoding, and the idle deadline wrapping the raw body before any decode. The one thing that is
320
363
  structurally different is under the hood — a single h2 connection multiplexes every concurrent
package/README.zh-CN.md CHANGED
@@ -251,6 +251,38 @@ TLS 指纹塑形不是这里的门槛——但 curl 的 **HTTP/2** 指纹能过
251
251
  curl(8.7.1 / nghttp2),照线上抓包原样复刻。这么做是实证需要,不是讲究:一个想当然的 h2
252
252
  指纹,可能恰好败在 curl 能过的地方——那这整件事就白做了。
253
253
 
254
+ ### 指纹
255
+
256
+ 两侧都对齐 **curl 8.21.0 / OpenSSL 3.6.3**,且全部可自定义。TLS 后端比 curl 版本更要紧——同一个 curl 用 SecureTransport 编译出来的 ClientHello 完全不同——所以参照物是从线上抓下来的,不是凭记忆写的,并且钉死在 `test/tls/fingerprint.test.js` 与 `test/http2/fingerprint.test.js` 里。
257
+
258
+ | 层 | 默认 | 配置项 |
259
+ |---|---|---|
260
+ | ClientHello 扩展**顺序** | 与 curl 完全一致(就双方都发的那些而言) | `tls.extensionOrder` |
261
+ | 密码套件 | 本包实现的 AEAD 套件,按 curl 的相对顺序 | `tls.ciphers` |
262
+ | supported_groups | `x25519, secp256r1, secp384r1, secp521r1` | `tls.groups` |
263
+ | 签名算法 | SHA-256/384/512 上的 ECDSA 与 RSA-PSS/PKCS#1 | `tls.sigSchemes` |
264
+ | ALPN | `h2, http/1.1` | `tls.alpn` |
265
+ | HTTP/2 `SETTINGS` 的 id **与顺序** | curl 的:`MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, ENABLE_PUSH` | `http2Settings` |
266
+ | h2 前导、`WINDOW_UPDATE`、伪头顺序、HPACK 表示 | curl 的,逐字节一致 | 固定 |
267
+ | `Accept-Encoding` | `gzip, deflate`——curl 的 | `decoders` 会追加 |
268
+
269
+ 扩展顺序之所以要紧,是因为 JA3 和 JA4 哈希的正是**线上顺序**的扩展列表,那是指纹识别读到的主要内容。`pre_shared_key` 无论你怎么配都强制排最后:RFC 8446 §4.2.11 把 binder 的转录定义为"截到 binder 之前的那段 hello",只有后面不跟东西时这个范围才成立。
270
+
271
+ **默认值刻意与 curl 不同的地方**,以及为什么不能照抄:ClientHello 是一份**要约**,服务器可以接受其中任何一项。声明你做不到的事,等于拿指纹不一致换一次握手失败——后者更糟,而且是静默的。
272
+
273
+ | curl 发送 | 本包 | 原因 |
274
+ |---|---|---|
275
+ | 30 个套件,含 RSA 密钥交换与 CBC | 6 个 AEAD 套件 | 有意不实现。服务器选中 `TLS_RSA_WITH_AES_256_CBC_SHA` 会得到一条死连接 |
276
+ | `X25519MLKEM768` 群及 1216 字节 key share | 不提供 | 未实现 ML-KEM |
277
+ | SHA-1 签名方案 | 不提供 | 明确拒绝 |
278
+ | `encrypt_then_mac` | 不发 | 只对 CBC 套件有意义,而 CBC 不提供 |
279
+ | `post_handshake_auth` | 不发 | 会招来握手后的 `CertificateRequest`,未实现 |
280
+ | — | `status_request` | curl 不索要 OCSP staple;本包必须要,因为 staple 是它唯一的吊销信号 |
281
+
282
+ 要抹平这个差距,得先实现 ML-KEM、RSA 密钥交换和 CBC 套件——那是另一个项目,而且后两者是本包**故意**不做的。`tls.ciphers` 和 `tls.groups` 允许你照样把它们报出去;服务器一旦选中握手就会失败,后果由你自己承担。
283
+
284
+ 有一个测试断言这张差异表**恰好**就是上面这些,所以哪天获得了其中某项能力却没更新这张表,构建会直接失败。
285
+
254
286
  HTTP/1.1 的 body 有的,HTTP/2 的 body 全都保留:流式(SSE 原样可用)、trailer、gzip 解码、
255
287
  以及在任何解码之前先包住原始 body 的 idle 截止线。结构上唯一不同的在水面之下——一条 h2 连接把
256
288
  发往同一源站的所有并发请求复用在一起,而不是一次签出、一次只服务一个请求。`install()`、重定向、
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tunnelfetch",
3
- "version": "1.1.2",
3
+ "version": "1.2.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",
package/src/client.js CHANGED
@@ -82,6 +82,11 @@ const NULL_BODY_STATUS = new Set([101, 204, 205, 304]);
82
82
  * the wire bytes it saves do not pay that back — see the README. The reason to turn it on is
83
83
  * matching a browser's Accept-Encoding, not saving CPU.
84
84
  * @property {boolean} [keepAlive] default true.
85
+ * @property {Array<[number, number]>} [http2Settings] the HTTP/2 SETTINGS flight, as [id, value]
86
+ * pairs. Order is significant — an Akamai-style h2 fingerprint reads the ids in the order they
87
+ * are sent — so this replaces the flight rather than merging into it. Defaults to curl's. The
88
+ * TLS half of the fingerprint is configured through `tls` (`ciphers`, `groups`, `sigSchemes`,
89
+ * `alpn`, `versions`, `extensionOrder`).
85
90
  * @property {boolean} [http2] offer HTTP/2 via ALPN and speak it when the server selects it.
86
91
  * Default true. The goal is ACCESS, not speed — some sites treat HTTP/1.1 as a bot signal — and
87
92
  * on a CPU-billed runtime h2 costs MORE than h1 (HPACK is extra work). Set false to offer only
@@ -436,6 +441,11 @@ function registerHttp2(client, key, conn) {
436
441
  { readable: conn.readable, writable: conn.writable, close: conn.close },
437
442
  {
438
443
  info: conn.info,
444
+ // The h2 half of the fingerprint. Passed through so a caller can present some client other
445
+ // than curl without reaching past the Client for it — the TLS half is configurable through
446
+ // `tls`, and one being reachable while the other was not made "the fingerprint is
447
+ // configurable" only half true.
448
+ ...(client.options.http2Settings ? { settings: client.options.http2Settings } : {}),
439
449
  onClose: () => {
440
450
  client._h2conns.delete(h2);
441
451
  // Only drop the keyed entry if it is still this connection; a newer one may have replaced it.
@@ -117,6 +117,10 @@ export class Http2Retryable extends Http2Error {}
117
117
  * @property {number} [maxConcurrentStreams] our advertised SETTINGS_MAX_CONCURRENT_STREAMS.
118
118
  * @property {number} [maxHeaderTableSize] our advertised SETTINGS_HEADER_TABLE_SIZE.
119
119
  * @property {number} [maxHeaderListSize] self-protection cap on a decoded response header list.
120
+ * @property {Array<[number, number]>} [settings] the SETTINGS flight sent in the connection
121
+ * preface, as [id, value] pairs. Order is significant — an Akamai-style HTTP/2 fingerprint reads
122
+ * the ids in the order they are sent — so this replaces the flight entirely rather than merging.
123
+ * Defaults to curl's: MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, ENABLE_PUSH.
120
124
  * @property {number} [maxHeaderBlockBytes] cap on the RAW bytes of one HEADERS+CONTINUATION run,
121
125
  * before HPACK decoding. Default 262144, matching the decoded cap. This is the bound that stops
122
126
  * a CONTINUATION flood; `maxHeaderListSize` cannot, because it is only reachable once the whole
@@ -196,6 +200,8 @@ export class Http2Connection {
196
200
  // reference makes a small input decode LARGER, never the reverse — so a block whose raw size
197
201
  // exceeds the decoded cap could not have produced an acceptable header list anyway.
198
202
  this._maxHeaderBlockBytes = opts.maxHeaderBlockBytes ?? 262144;
203
+ /** @type {Array<[number, number]> | null} the SETTINGS flight, ids and order included */
204
+ this._settingsFlight = opts.settings ?? null;
199
205
  this._expectFirstSettings = true;
200
206
 
201
207
  this._fatal = null; // set once; rejects every stream and every future request
@@ -230,11 +236,19 @@ export class Http2Connection {
230
236
  _sendPreface() {
231
237
  // Exactly curl's flight and order: the 24-byte magic, then SETTINGS (ids 3,4,2), then a
232
238
  // connection-level WINDOW_UPDATE that raises the receive window to 1000 MiB. See constants.js.
233
- const settings = settingsFrame([
234
- [SETTINGS.MAX_CONCURRENT_STREAMS, this._ourMaxConcurrent],
235
- [SETTINGS.INITIAL_WINDOW_SIZE, this._ourInitialWindow],
236
- [SETTINGS.ENABLE_PUSH, 0],
237
- ]);
239
+ //
240
+ // The ORDER of the settings, not just their values, is part of the fingerprint an Akamai-style
241
+ // h2 hash reads, so a caller matching some other client needs to be able to set both. Supplying
242
+ // `settings` replaces the flight wholesale; the values still drive this connection's own
243
+ // behaviour, so a caller who advertises a window it will not honour has misconfigured the
244
+ // connection rather than merely disguised it.
245
+ const settings = settingsFrame(
246
+ this._settingsFlight ?? [
247
+ [SETTINGS.MAX_CONCURRENT_STREAMS, this._ourMaxConcurrent],
248
+ [SETTINGS.INITIAL_WINDOW_SIZE, this._ourInitialWindow],
249
+ [SETTINGS.ENABLE_PUSH, 0],
250
+ ],
251
+ );
238
252
  const inc = this._ourConnWindow - DEFAULT_INITIAL_WINDOW;
239
253
  const flight =
240
254
  inc > 0
@@ -135,6 +135,12 @@ function expectServerHello(msg, offers12) {
135
135
  * @property {number[]} [offerGroups] groups to send an actual key_share for. Default the first
136
136
  * supported group; a HelloRetryRequest recovers any other choice at the cost of a round trip.
137
137
  * @property {number[]} [ciphers] cipher suites to offer, in preference order.
138
+ * @property {number[]} [sigSchemes] signature_algorithms to offer, in preference order.
139
+ * @property {number[]} [extensionOrder] ClientHello extension types, in the order to emit them.
140
+ * JA3 and JA4 hash the extension list in WIRE ORDER, so this is most of what a fingerprinter
141
+ * reads. Defaults to curl's order (`CURL_EXTENSION_ORDER`). Extensions not named keep their
142
+ * natural position at the end; `pre_shared_key` is always last whatever is asked, because RFC
143
+ * 8446 s4.2.11 defines the binder transcript as the hello truncated just before the binders.
138
144
  * @property {Uint8Array} [clientRandom] fixed ClientHello.random, for reproducible handshakes.
139
145
  * @property {Uint8Array} [legacySessionId] fixed legacy_session_id, likewise.
140
146
  * @property {boolean} [compatibilityCcs] send the middlebox-compatibility ChangeCipherSpec.
@@ -326,6 +332,8 @@ async function drive({ record, hostname, verifyPeer, options, deps, versions })
326
332
  alpn,
327
333
  ciphers,
328
334
  versions,
335
+ extensionOrder: options.extensionOrder,
336
+ sigSchemes: options.sigSchemes,
329
337
  random: options.clientRandom,
330
338
  legacySessionId: options.legacySessionId,
331
339
  psk: pskOffer && {
@@ -210,6 +210,65 @@ export async function deriveSharedSecret(group, privateKey, peerKey) {
210
210
  * @param {ClientHelloOptions} opts
211
211
  * @returns {ClientHello}
212
212
  */
213
+ /**
214
+ * Extension emission order, by type. This is not cosmetic: JA3 and JA4 hash the extension list in
215
+ * WIRE ORDER, so the order alone is a large part of what a fingerprinter reads.
216
+ *
217
+ * Captured from curl 8.21.0 / OpenSSL 3.6.3, which sends:
218
+ * renegotiation_info, server_name, ec_point_formats, supported_groups, ALPN, encrypt_then_mac,
219
+ * extended_master_secret, post_handshake_auth, signature_algorithms, supported_versions,
220
+ * psk_key_exchange_modes, key_share
221
+ *
222
+ * Two of those this package does not send, and the reason is the same in both cases — an extension
223
+ * is a claim about what we can do. encrypt_then_mac only applies to CBC suites, which are not
224
+ * offered; post_handshake_auth invites a CertificateRequest after the handshake, which is not
225
+ * implemented. status_request goes the other way: curl does not send it, this package does,
226
+ * because a stapled OCSP response is its only revocation signal. It is placed where OpenSSL puts
227
+ * it when it does send one, right after server_name.
228
+ *
229
+ * Anything not named here keeps its natural position at the end, and pre_shared_key is forced last
230
+ * whatever the caller asks for, because RFC 8446 s4.2.11 defines the binder transcript as the hello
231
+ * truncated just before the binders — a range that only exists if nothing follows them.
232
+ */
233
+ export const CURL_EXTENSION_ORDER = Object.freeze([
234
+ EXTENSION.renegotiation_info,
235
+ EXTENSION.server_name,
236
+ EXTENSION.status_request,
237
+ EXTENSION.ec_point_formats,
238
+ EXTENSION.supported_groups,
239
+ EXTENSION.alpn,
240
+ EXTENSION.extended_master_secret,
241
+ EXTENSION.signature_algorithms,
242
+ EXTENSION.supported_versions,
243
+ EXTENSION.psk_key_exchange_modes,
244
+ EXTENSION.key_share,
245
+ ]);
246
+
247
+ /**
248
+ * Put the encoded extensions into the requested order.
249
+ *
250
+ * @param {Array<Uint8Array|null>} parts encoded extensions, nulls for the ones not offered
251
+ * @param {number[]} order extension types, most significant first
252
+ * @returns {Array<Uint8Array>}
253
+ */
254
+ function orderExtensions(parts, order) {
255
+ const present = parts.filter(Boolean);
256
+ const typeOf = (e) => (e[0] << 8) | e[1];
257
+ // pre_shared_key is not the caller's to place.
258
+ const psk = present.filter((e) => typeOf(e) === EXTENSION.pre_shared_key);
259
+ const rest = present.filter((e) => typeOf(e) !== EXTENSION.pre_shared_key);
260
+ const rank = new Map(order.map((t, i) => [t, i]));
261
+ // A stable sort on rank, with unranked extensions after every ranked one in the order they were
262
+ // built. Array.prototype.sort is required to be stable, so equal ranks keep their relative order.
263
+ return [
264
+ ...rest
265
+ .map((e, i) => ({ e, i, r: rank.get(typeOf(e)) ?? Number.MAX_SAFE_INTEGER }))
266
+ .sort((x, y) => x.r - y.r || x.i - y.i)
267
+ .map((x) => x.e),
268
+ ...psk,
269
+ ];
270
+ }
271
+
213
272
  export function buildClientHello({
214
273
  hostname,
215
274
  keyShares,
@@ -220,6 +279,7 @@ export function buildClientHello({
220
279
  sigSchemes = SUPPORTED_SIG_SCHEMES,
221
280
  alpn = [ALPN_HTTP11],
222
281
  versions = [TLS13, TLS12],
282
+ extensionOrder = CURL_EXTENSION_ORDER,
223
283
  extraExtensions = [],
224
284
  psk = null,
225
285
  randomBytes = defaultRandom,
@@ -285,7 +345,7 @@ export function buildClientHello({
285
345
  .vector(1, sessionId)
286
346
  .vector(2, suiteBytes.build())
287
347
  .vector(1, Uint8Array.from([0])) // legacy_compression_methods: null only
288
- .push(encodeExtensionBlock(extensionParts))
348
+ .push(encodeExtensionBlock(orderExtensions(extensionParts, extensionOrder)))
289
349
  .build();
290
350
 
291
351
  const message = handshakeMessage(HANDSHAKE_TYPE.client_hello, body);
@@ -239,6 +239,10 @@ export async function continueTls13(ctx) {
239
239
  alpn,
240
240
  ciphers,
241
241
  versions,
242
+ // The retry must reproduce the first hello's extension set AND order: s4.1.2 does not list
243
+ // either among the modifications a second ClientHello may make, and a strict server checks.
244
+ extensionOrder: options.extensionOrder,
245
+ sigSchemes: options.sigSchemes,
242
246
  random: hello.clientRandom,
243
247
  legacySessionId: hello.legacySessionId,
244
248
  extraExtensions: cookie ? [cookieExtension(cookie)] : [],
@@ -18,61 +18,61 @@ export const WARMUP_HOSTNAME = "warmup.invalid";
18
18
  export const WARMUP_NOW = 1893456000000; // fixed epoch ms inside the chain's validity window
19
19
 
20
20
  const CLIENT_PRIV =
21
- "MC4CAQAwBQYDK2VuBCIEIPjlXa5xRQTO0OmW85dWmeM6C0AjCqfOKH5MJlnIpT9b";
21
+ "MC4CAQAwBQYDK2VuBCIEINivgNQO5n8jwnzWfEWjdR14q7Km+UhPnqyL3RtkDbF7";
22
22
  const CLIENT_PUB =
23
- "vbmT3r0piIyf5GavMXh1RqIsCml4dFibAAgovDW+Vww=";
23
+ "o5wNrq9yxfTLf8U3jLhUsXB52Kmf2q3zodyy/4jELSM=";
24
24
  const CLIENT_RANDOM =
25
25
  "AwoRGB8mLTQ7QklQV15lbHN6gYiPlp2kq7K5wMfO1dw=";
26
26
  const SESSION_ID =
27
27
  "BRAbJjE8R1JdaHN+iZSfqrXAy9bh7PcCDRgjLjlET1o=";
28
28
  const CLIENT_HELLO =
29
29
  "AQAA9AMDAwoRGB8mLTQ7QklQV15lbHN6gYiPlp2kq7K5wMfO1dwgBRAbJjE8R1JdaHN+iZSfqrXAy9bh7PcCDRgjLjlET1oADBMB" +
30
- "EwLAK8AvwCzAMAEAAJ8AAAATABEAAA53YXJtdXAuaW52YWxpZAAFAAUBAAAAAAAKAAoACAAdABcAGAAZAA0AFgAUBAMFAwYDCAQI" +
31
- "BQgGCAcEAQUBBgEAEAALAAkIaHR0cC8xLjEAKwAFBAMEAwMAMwAmACQAHQAgvbmT3r0piIyf5GavMXh1RqIsCml4dFibAAgovDW+" +
32
- "VwwALQACAQEAFwAAAAsAAgEA/wEAAQA=";
30
+ "EwLAK8AvwCzAMAEAAJ//AQABAAAAABMAEQAADndhcm11cC5pbnZhbGlkAAUABQEAAAAAAAsAAgEAAAoACgAIAB0AFwAYABkAEAAL" +
31
+ "AAkIaHR0cC8xLjEAFwAAAA0AFgAUBAMFAwYDCAQIBQgGCAcEAQUBBgEAKwAFBAMEAwMALQACAQEAMwAmACQAHQAgo5wNrq9yxfTL" +
32
+ "f8U3jLhUsXB52Kmf2q3zodyy/4jELSM=";
33
33
  const SERVER_BYTES =
34
- "FgMBAHoCAAB2AwPR9xYqSLPpiCwNco2cXVmtsxGRHpcVn9pgzYTTNfuygSAFEBsmMTxHUl1oc36JlJ+qtcDL1uHs9wINGCMuOURP" +
35
- "WhMBAAAuACsAAgMEADMAJAAdACB+60XRWGB4e1t9Dy9U4hH3KMr8mYswz6ypnzfDjwHbdRcDAwdYnGQEkD2xWtrlMBiqAi7aAE5y" +
36
- "OFQ7yBVgD+JYtaKoaQ5E6XGWU8HFO8TwqJAN1k1PERiecKl0Pdj1ne7lLDUxJ8w4rJMjXxuc8vSyfN1KjTxoBXvZ4hKuJeolN4Sp" +
37
- "nVeUNtFXfNnLNuk1UDJZ/UWsZKATQM2PQEAgSXJA5bogqMD3w81B9FHF/6TxFiWu1uhB9AavIgwiRrO1XJPt/toozSJl/CMfvJNp" +
38
- "+ukMVYeKACc8Vu9tYYi1gZRbVh9jDpWfW4x5Qsvwb+GgWdvDeDe4+P5nff2cmq2KrlUyGXaezYZiusNpYsRtGDLz+mcH1Gj99m4v" +
39
- "fa4bGGz4lZiZG03M3CYJ6xbTfXn9JnRvhMjNxU4PxN1ntYYjuAPF+1GgKcjpTDWWY2sWNycZXY9nIk7AwG2+bl062gwmTHcSYXut" +
40
- "1G7+WSzncPbzsdN00JJFjJDYMuH+mvtCqRaaOLu3A1Doqmdf79yfBUZN9pNA+DsFTYYaUGD4yaOzp+KguRAuVxd3yA0fsPRmz276" +
41
- "oDmTCkNqgH8AZ3LPBHuW2b/DKG8Md4i6/p1pjHWa3Odjh8AIv6fc59deRwt9w6++FHODQC+NLa+2YFkuPWjxcWvYNBNulTQddz2y" +
42
- "hPIBu8T7cL4lFgK0sgif13Q3ZSRMNbqdmS5T2weodLVnmODNs3o3pA42Q6yt1yPnoKtdk+yQuN2xvOBZb3h3ZMhIMcFHHeGlHoiU" +
43
- "uogypE89jOsorucWK6yroBvewBOA4bx8TtUC46LkGSe5VTqatsixkODqd2RUMISXj+jE/g+niaOUWSQ4ONVucJBd6SgWwCT+EkJb" +
44
- "oxH2ZyfInSNqRUAu5bUsb6P9kodpAtp2FKbLmCALoKL6K7RcHLfPtDh6Ejv8zHay7UGSINZtoKFhkVO5cpoNFoBgxAQud01RVh09" +
45
- "BCz0IhBw8ZIsK+4GLd43ffHjO4Oqx5JQUIXpRO4i5/wqRxFac8SfmqUqkDJ4EqZlbZ0VO151hkslyHUjwb540kFtsO4+6pFw4dZt" +
46
- "Hu9h4qW6wspnm2GWJb1KZtHeQfOD3bNvNCsLPxCSgTkGyXZAzhW1uMzXXElONkw+vaE5y/9W2vadqLR1TMzwYsW3K2qc/zIq7/wx" +
47
- "q0nFJubW9Gty2D2gF3Zikfhid6YDoUYVg5Wy5uXayXR/4Mss7b4VF0ikUCsVn9JVwSony5o4TlHa4Zh9nzmJFXoVKQwmOeQ0/T6g" +
48
- "ap86oZRdXSpMZDg0LarqVOOsU4zyuK0a9Ts8TTmoDxz5wpVRKI87mDb1kN/T8YfDM+Kaf181nhQM+jfXUtj3g2SjrbfmOgDUWcy2" +
49
- "6solXDjF0XWEe42tcQm44k0518zHUL9ItudBsKkLZpFcfmTDHON7SOUjydsLEB5vMd9m/rfDl6c9nGZyUic+LLjpocc41zvyN/cx" +
50
- "qZl/dwFxrHooPEYhaiqOxSbn75doMygIhjwLQHMPrG72d6Ym1UrMcUwhi/4oad6xE7tlIGLbJqv1RCgg6dij5tGpPOvXoBcvQVEs" +
51
- "AWpq8pTXEpru5yydwrX9akoC1J6pKuhwYudrzfA3jvPTcCnHWAKS3PG+hQ59JLuxVUrw6f9A3/jXUUuYk3XkGuKLw924STdQS+9d" +
52
- "i34c7DB6TcDaoq9XMMTs4DsU/aaILIUztt1rgjEyRxx16Kjm8qot5g2ldz/suM7iHSe2Dd+MOvdGmA2cnF6EW2VFMDbbRUe1BL6m" +
53
- "a8Sa62xZPgF+hZrK+cQNs1MYIVjrzdbDxwePWSl+R87dgNBtplZKh/fp4as9tM/SD7kLh/TxeTEBIuuf5VKPGQcHLt4ifYh+4eIj" +
54
- "h9IKJchum7/53DW+13efK2qVXtjAz8izCDmL8My5QrfeWwKhppGzSILDPtEce/MUYu5bUNo4n8AHomWf2fpaeUn104bDHMH+0ook" +
55
- "cpAJki15og5ZrxzkUG6oNtCGgZqfogV3FDRw4olaEkUQY7cHTi7CZkR3oRNiOMJu/fsi6XbksXxNDW9y+ugUwciW9UjqxkfLh7I4" +
56
- "/eETaNRbuVYjcVSypTEuC1dqjXnxFucHN0NyANn1PrIm7qBOoPnrLhq5wYpuLZ9qlfiXWun3oBlO4MyRh0YepClhx/qT74KZryFB" +
57
- "0AyYzMolgyvR6OiZ3ajxEqo2pUgfKtDlo1AqGmydH16HScaaIgG8R2nj30FUE4BOJUr8XWm4Nk9yVuI0LPrVSaIb7RlpfskAVAGu" +
58
- "/wF/YR9Itk9GhGiQ4pw50hgi/MnzQYixea9bUV+1Lfe4aA+/WRoQYsAu2jO+4VsuhwSCv5zTQPllr+rkNmvwjEnX7IAl138dk+/Y" +
59
- "gA16PAWCQUkrk6Z1vZFjl3qfYelCwuIWT7HnfW8ZTYjvDZ5OgnLtm55MKjdrwX6xhxXocZ8+SiurIx9FVQdvFJg3vvAPCYnislUf" +
60
- "wbIQQlCI0JWcyL0iI/JmIE2YWsegK1pvD/3PmB1fUiePd0wDado3LpdO39yUYA6rE4ynHUpwrmnCPlqEOh0XAwMBGacbGrwvRADw" +
61
- "I/oOygBJY1Cl6puADo3n6DTofw4wr1ihuyxX6c2Br8Fj9R57WG8ALIp0CQNNy7mJ8YynOlKP1AeQspIelnQPzVo8l4n7ThvlMayx" +
62
- "xDMnJHdIBCSKg8rUQdxi/zM/z1lnORCzQMJXi2A786I+FTa/2f+y9ln2uDRD6LEKlrz71onlf8o2h2x+cqwYUBdQPeZqa6gkFhWJ" +
63
- "5QXq0jK/iViKEIyduX7wZL9Pf26PKvsuiFUTNwBMvEJdAkSkmhxbR4SeWdMuvxQ/cvIn42Id6HaOqdsEueNrv/dnskCqqAHDGFLe" +
64
- "h+IQgpHZdFNYrFPlhhDLqIHLk6MZZLfS3QLmFhVTSMn4xpQ809ZMvGT3NXWTOB7GFwMDABM00dpa951QoPFgnJv9SpPMaxoa";
34
+ "FgMBAHoCAAB2AwMC2KMpeoduVRsS/IdMsSXpe01e8s3wE/rytik1l91rgiAFEBsmMTxHUl1oc36JlJ+qtcDL1uHs9wINGCMuOURP" +
35
+ "WhMBAAAuACsAAgMEADMAJAAdACAr+v3w+m4O5oxHFSy624+fpkWPq277w7+v3Ke3hrKpOxcDAwdY/NxJTmM9YwAtwCbLgQJoioff" +
36
+ "HlNuzTL1ruUzSLqTb4yMSTiKyiOITsAVKStDY8NsguJ1/CKx/s93Oy4wgtcmJudrxMQK5tRTYfwCrZDgR5yfSVD25wIX5VjuTuJ4" +
37
+ "OsQFJ0qX5cqXv4042XcyR06iRqN7iOKY8h2aH5opqQylx5+51GyKjFWB+qIwTceJ8+r/sl/wkxh4hwq7AfmZuRE8suxcZlojgrH6" +
38
+ "ka1eJ81f/HbalU90Xjs3L9A4UnxB/e3CtoA12a8TkvD6bQCb3sCZUwgt0GSKajpw8/n11XWEyW5AAojiopI+pxwur5wMEkdNrABX" +
39
+ "FpDt+pBW2hfWgUgeHO0JLn2XkXQVnipgdd6e6mtePKtIFqT4tW7dW5Rzp1X8QwqSA+oGpwUTleL4qROziaY9cdALrVHFQ3P5L4IA" +
40
+ "fWPF7FmGj+PBOqo1V4Xlw9suTnGJmiIve9XOY+J0GndLb3b/DULmo0wNG4VdvyO0keFqg2Ov3cKAFHfrbPCRSJwdQfpaA2abuC9H" +
41
+ "MkKgK5JAtvWbAopxu281UMA3tJpWSxiI8LjpbLnNqhUFseBukdRFVVPwE4ckqwqk5NcTb66Eqyyw0fd+TioDTqyTyyGJJwvBHm3l" +
42
+ "KXkU4x4crwFsyDWDZpH8KbYbImVlpCtK+uSPEouREgTeimbeR3elxccpfGkm47yYnERBAfCcM18OcZkMb3AqFsgnaN+Cy345lBJs" +
43
+ "uJfRkRfAmdhlCz5KWmqSNY+3fT6iWyJ9dPBvmcsCnxn6FUZfNxAoYSi7wNzsSv7CRMDWPhdEIWMoDVjf8ZKubz0R/OddtVr1Il20" +
44
+ "xiIcevpEJ9pRntZjCkWtykA+z48Vw3Ia8HoMsKDMQagJ472FhNggwMipgA1uM6+nEe+/scUC3HDck6YGwgpb9k1LWVxYsCIdoz0t" +
45
+ "pbVoCV+QCvqyJGNWpIwRQLYjxHV84WzH/tWbGyyFXkFrA/7XgjE4bydjs88lcC7rAHKXLRrpF1CMwQg5XOkI8ftYRbYIKK9OYx/7" +
46
+ "uUPGBPdPvYkrOwe2q1VgHQd+vK87hlN+aNDOlbxSdcy9U84OSUPwDQs7TYqu7R9Hxsc8BTnRo1Gn6XeVrIdVZmrhQQWdCX9MFSww" +
47
+ "GCs8IxY5CM/2/thifsZN85K0AxgBz45XphZS9YNAjCIex8L4ky5mruYESLcHErLPkMZCDUuvgapmtD8SKsxTz/sLhZuotN6Xmbmb" +
48
+ "5WjTm3Rw7W95mI41mHRCLBEHK2KhqSLdpH2vO43D/g4LKAh21M3wxKJAmElkRyUa8zb+E6GcqtTnL6Zldlm+cRp8KopVLBAJFpFw" +
49
+ "T3riPPiJ6QxWcZjM9aXCHPE+TL1W+KzyTwLChHzPjBopJ+PMjXos7T7DTgQLmO9jsdILAl6xNQgPPukoOAy9Wn3yCWcsETEelZbV" +
50
+ "rgZ4aEwXkurJ5RrsOjGEN+uODmJ2lbW2//B+lKlDGILtisLEg8CzzrSN6BZ420Z50PkGZoP6ML7+JC0rtC75gyNrWXYIBzxwr2/S" +
51
+ "2F30780r8cqU5yvq7VI+nDbUzNoauhWgdVBrCQ/4blaupU+yxHbBvYjRBMBuKwcHTJO2R/s+xTqK5j41Sukm3x99+vEqwrTmN1dm" +
52
+ "aWYk70oKuW1Ewx9V7/ExKwGbsDBxYMjKS8h/aAc0CZz7Fxk30V05LA5rUPsG7zeUyKsvBnnivtCcY7GN1b6mEsLhkkHg6hxaDjBf" +
53
+ "GfjodpXTHsbiZExkL+CYQnE6LQGtOhHrt+ojtQb7iSCvdjIZ/i1Mw2N3UFjkW6qM1ISASQBCSrG8WZqvjZQ+wsngaeo1iL5Zg5qv" +
54
+ "Rc+KoaEackS0NVraKSFrc+xHfyofDskV/FsS9ovfYnouP3pV5SdIcBWMOvIUovGLB2m1nW1k1mySRfm4VHqeag/cSmXSNQX6aVSh" +
55
+ "AsrHAyPrwtBBF5Ox892Ok5p91yDahZhDWfTBTTkY76Ru12FaVafiPyG6lFxbx88tIpiVqfnhqvlj2qvLJFe24MmvjWiXdO00axAy" +
56
+ "M9JTe0IvA5hvyVaTn0w1MzzaNBkDDPpgLlt0yEVoblcrqaQjP0H9J+SjxAUgT52sDDCLjFxFY/zh5guI9RhDYZ2Pyg+vdudJKfja" +
57
+ "dr4L4CpUTKFO+DEJf/Efc7nOlTJWD4Dh3SavGfu7gplzh3FMBFegkUkCQJC2hzatNNSpkE8REhC73Rm9me2iysJMv/4Sy5a6Er5Y" +
58
+ "ffE0aAsJ/gXCvE7fQbeZhogp3iBQm911LBwtUn5Wwh8ALGELJxSsQloxFJwPYSzhUGJ01XKNYo6GIWjrpWtcKIKAe1tlB4Ni/Ag7" +
59
+ "bQV7J67dyHvMti/Jgq7VODUJIb8E+u6H7FVQ/8jzE1mm4Dmmtfd9LqB7pal3YbzzxwfN6Qw84jN7lgcksByuZ7eTV2Q4T0IK/toG" +
60
+ "oroTtRRz7vMwg6xLnNIhtdeQaQMVnveuVkWZ29pzwNTxaKVITIIfsJjxTmHqph7wED0rBpR7vSs9WOsMSeoXAwMBGfgD+3KLo6SI" +
61
+ "9AOwKrhRqXO7Xgn2i58peIpNbvy4DVJ3AOjAect4osO+zRcr5gBp/2JFJgL5l5hgdUpApp9U50/SOOPwCvAZIpfPDlWlixJjUF/K" +
62
+ "8NxWHgx62Wuq6mX7CW7BEqwNzb1yYNSzfZYR0qhmiJ04vj4nnxXcmKum9fj5eO4ptiyLUAqrDpYwUAKCAqlLVZCJbS1dVdwEWSQs" +
63
+ "X/lq6RoYYBaRde8fJT2EXzQnfkn5eV4dzI1gseEMQUJ85kshKa847TwI/4rNRhdhXtDCHi8lMfoRgcobGwYmSImTtt6pxJPj07Og" +
64
+ "tyDBW2lyhFOLT2hMSJSdK/tQ/PezQDviGzEaV/5oKHDReaTKWOsjbDkftJiufdJ+FwMDABOfvj5nYBpetvkAsU02oXftLJYe";
65
65
  const ROOT_DER =
66
66
  "MIIC2jCCAcKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAeMRwwGgYDVQQDDBNXYXJtdXAgRml4dHVyZSBSb290MB4XDTI1MDEwMTAw" +
67
67
  "MDAwMFoXDTM1MDEwMTAwMDAwMFowHjEcMBoGA1UEAwwTV2FybXVwIEZpeHR1cmUgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEP" +
68
- "ADCCAQoCggEBAMlyqJWB5tdme7o+TptR4T2er2MD5gsVcRmg1XWAIHnjY5Wy+cQ9dn670JtY5My0WBX+RSDPFsRzPnimzyLm2Jzs" +
69
- "J8xpOMDtfE3/NUjxLSHrhdxJ+gFqoB0j7faIzLar1EXMdlhGCmFtXXoMQyuKhlMRzqP3VpH0N91OkGueQFlJAsuazEf9zm0v6PnV" +
70
- "1QuH4X4+/wRYeAjrLmksLkLhI2h/QZ/EwiVwcLQUvPb4kSHeemZKRRXByLAqOBj6g/MqFRkhqiYQmDB4upjBcYHC8Ae9b02hP8vQ" +
71
- "q9Dv5pRGloGk7olcviiSTj3YTIFd5Cd7xQZXyE42dJa6C9bZHkgZHPMCAwEAAaMjMCEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B" +
72
- "Af8EBAMCAgQwDQYJKoZIhvcNAQELBQADggEBAB62L1/vofc1iZN1EoyIdHfgQZS7EtrjO4kkpHJPb2e1AmgvSCGSBC1OBaqlgpcT" +
73
- "gSfmwSY7g059w18gaEkBhjm5czUmt+IM1DWir6hLPI//6QizsQo0AatePnMvJYuny9VMG2HWbpY17IKxT3/sG4zID7XtDjycuBkf" +
74
- "uy6DN6Qrp0+aqYMfXs28HWwDgf3b7ZjEAwquSQ+dG8Ml36fd8hjydf2VjOl0h5C3xfc88sEEv0BAks11cYjHDT6jHNYYbXfWcesk" +
75
- "nflRe2my0pLsKJFzVhQvpWl6Tud5+Im99hi0GMI1L17hMv9YlCGR+vIQsP78y/WTIEHMiFTpCWd1NTY=";
68
+ "ADCCAQoCggEBAMEAjrxVUntvHweCVz7mjYZTWZiTOW3mabXocCrFXKs8CcJVYNGQkpvhC/1VIr4t7hdrSPG31j6PvR4x0OAZTJZR" +
69
+ "k++Zo1jPUWIG3YomFJWhVjoJqoZTcO8uMDydvO75OTxto6Mc5gpfzHgfc52aAqemSqnfzryjupS6o1KOwbE1byLjM/Mmpp4CrRXx" +
70
+ "Fy/Jd8g2w1dSTTZo7QDIYBuZcNW4UYna9Mpac7otnv8JdlRSKonbbxXbx1E2GUm0tVM+4B5ARtBOFggnZYNW+KvQwcl4NRLTrFZS" +
71
+ "KNaRAW78DxclmSBsMm0lV+lsnV9miyuR2vfsj1FwSHKe1gzf12yRMpECAwEAAaMjMCEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B" +
72
+ "Af8EBAMCAgQwDQYJKoZIhvcNAQELBQADggEBAGmB1ws+kKHGtO5D8UnCCyXI/q5LtHsMDO+lDwp/6HAKJ6xVnJCNVP4d1/nTwMVm" +
73
+ "KeJTfEseakqaVSiUcowlPEgSM1OVIRU6aRqBog4cjyBt9F74cmdy+VOewkd4jwHUDnkuj8sNx6BdU8FN83w2nIlQ35g/exzQDP8q" +
74
+ "mB/GtQlnGbmi6mRMKzFn5stSwQS5tcTwz8h5dkDG2FtsBG4f8YDs8r2G0ur7FQD9aIhlttcUUdSE1mZHm4itgqgyE+hreL0x0wqz" +
75
+ "7V7NQK7fMH8OLTr5JrjVMyrpX8EQqyntEkAS8IaaQ42h3GNo1+HRVeJ9M53noaveacxhtTZDnI5u8wA=";
76
76
 
77
77
  export const WARMUP_FIXTURE = {
78
78
  clientPrivPkcs8: () => B(CLIENT_PRIV),
package/types/client.d.ts CHANGED
@@ -65,6 +65,11 @@ export function install(options?: ClientOptions): () => void;
65
65
  * the wire bytes it saves do not pay that back — see the README. The reason to turn it on is
66
66
  * matching a browser's Accept-Encoding, not saving CPU.
67
67
  * @property {boolean} [keepAlive] default true.
68
+ * @property {Array<[number, number]>} [http2Settings] the HTTP/2 SETTINGS flight, as [id, value]
69
+ * pairs. Order is significant — an Akamai-style h2 fingerprint reads the ids in the order they
70
+ * are sent — so this replaces the flight rather than merging into it. Defaults to curl's. The
71
+ * TLS half of the fingerprint is configured through `tls` (`ciphers`, `groups`, `sigSchemes`,
72
+ * `alpn`, `versions`, `extensionOrder`).
68
73
  * @property {boolean} [http2] offer HTTP/2 via ALPN and speak it when the server selects it.
69
74
  * Default true. The goal is ACCESS, not speed — some sites treat HTTP/1.1 as a bot signal — and
70
75
  * on a CPU-billed runtime h2 costs MORE than h1 (HPACK is extra work). Set false to offer only
@@ -222,6 +227,14 @@ export type ClientOptions = {
222
227
  * default true.
223
228
  */
224
229
  keepAlive?: boolean | undefined;
230
+ /**
231
+ * the HTTP/2 SETTINGS flight, as [id, value]
232
+ * pairs. Order is significant — an Akamai-style h2 fingerprint reads the ids in the order they
233
+ * are sent — so this replaces the flight rather than merging into it. Defaults to curl's. The
234
+ * TLS half of the fingerprint is configured through `tls` (`ciphers`, `groups`, `sigSchemes`,
235
+ * `alpn`, `versions`, `extensionOrder`).
236
+ */
237
+ http2Settings?: [number, number][] | undefined;
225
238
  /**
226
239
  * offer HTTP/2 via ALPN and speak it when the server selects it.
227
240
  * Default true. The goal is ACCESS, not speed — some sites treat HTTP/1.1 as a bot signal — and
@@ -74,6 +74,8 @@ export class Http2Connection {
74
74
  endStream: boolean;
75
75
  } | null;
76
76
  _maxHeaderBlockBytes: number;
77
+ /** @type {Array<[number, number]> | null} the SETTINGS flight, ids and order included */
78
+ _settingsFlight: Array<[number, number]> | null;
77
79
  _expectFirstSettings: boolean;
78
80
  _fatal: any;
79
81
  _goaway: {
@@ -252,6 +254,13 @@ export type Http2ConnectionOptions = {
252
254
  * self-protection cap on a decoded response header list.
253
255
  */
254
256
  maxHeaderListSize?: number | undefined;
257
+ /**
258
+ * the SETTINGS flight sent in the connection
259
+ * preface, as [id, value] pairs. Order is significant — an Akamai-style HTTP/2 fingerprint reads
260
+ * the ids in the order they are sent — so this replaces the flight entirely rather than merging.
261
+ * Defaults to curl's: MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, ENABLE_PUSH.
262
+ */
263
+ settings?: [number, number][] | undefined;
255
264
  /**
256
265
  * cap on the RAW bytes of one HEADERS+CONTINUATION run,
257
266
  * before HPACK decoding. Default 262144, matching the decoded cap. This is the bound that stops
@@ -14,6 +14,12 @@
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
16
  * @property {number[]} [ciphers] cipher suites to offer, in preference order.
17
+ * @property {number[]} [sigSchemes] signature_algorithms to offer, in preference order.
18
+ * @property {number[]} [extensionOrder] ClientHello extension types, in the order to emit them.
19
+ * JA3 and JA4 hash the extension list in WIRE ORDER, so this is most of what a fingerprinter
20
+ * reads. Defaults to curl's order (`CURL_EXTENSION_ORDER`). Extensions not named keep their
21
+ * natural position at the end; `pre_shared_key` is always last whatever is asked, because RFC
22
+ * 8446 s4.2.11 defines the binder transcript as the hello truncated just before the binders.
17
23
  * @property {Uint8Array} [clientRandom] fixed ClientHello.random, for reproducible handshakes.
18
24
  * @property {Uint8Array} [legacySessionId] fixed legacy_session_id, likewise.
19
25
  * @property {boolean} [compatibilityCcs] send the middlebox-compatibility ChangeCipherSpec.
@@ -144,6 +150,18 @@ export type TlsOptions = {
144
150
  * cipher suites to offer, in preference order.
145
151
  */
146
152
  ciphers?: number[] | undefined;
153
+ /**
154
+ * signature_algorithms to offer, in preference order.
155
+ */
156
+ sigSchemes?: number[] | undefined;
157
+ /**
158
+ * ClientHello extension types, in the order to emit them.
159
+ * JA3 and JA4 hash the extension list in WIRE ORDER, so this is most of what a fingerprinter
160
+ * reads. Defaults to curl's order (`CURL_EXTENSION_ORDER`). Extensions not named keep their
161
+ * natural position at the end; `pre_shared_key` is always last whatever is asked, because RFC
162
+ * 8446 s4.2.11 defines the binder transcript as the hello truncated just before the binders.
163
+ */
164
+ extensionOrder?: number[] | undefined;
147
165
  /**
148
166
  * fixed ClientHello.random, for reproducible handshakes.
149
167
  */
@@ -28,51 +28,30 @@ export function generateKeyShare(group: number, { generateKeyPair }?: import("./
28
28
  * @returns {Promise<Uint8Array>} throws on any degenerate or malformed peer key
29
29
  */
30
30
  export function deriveSharedSecret(group: number, privateKey: CryptoKey, peerKey: Uint8Array): Promise<Uint8Array>;
31
- /**
32
- * @typedef {object} ClientHelloOptions
33
- * @property {string} hostname SNI, unless it is an IP literal (then no SNI is sent)
34
- * @property {Array<{ group: number, keyExchange: Uint8Array }>} keyShares public halves to
35
- * offer; empty for a 1.2-only hello, whose wire form must not carry the extension at all
36
- * @property {Uint8Array} [random] fixed ClientHello.random, for reproducible handshakes
37
- * @property {Uint8Array} [legacySessionId] fixed legacy_session_id, likewise
38
- * @property {number[]} [ciphers] default: the union for the offered versions, 1.3 first
39
- * @property {number[]} [groups] supported_groups, default SUPPORTED_GROUPS
40
- * @property {number[]} [sigSchemes] default SUPPORTED_SIG_SCHEMES
41
- * @property {string[]} [alpn] default ['http/1.1']; empty array omits the extension
42
- * @property {number[]} [versions] default [TLS13, TLS12]
43
- * @property {Uint8Array[]} [extraExtensions] pre-encoded, sent verbatim (the HRR cookie)
44
- * @property {{ identity: Uint8Array, obfuscatedTicketAge: number, binderLen: number }} [psk]
45
- * offer this resumption PSK. Encoded with a zeroed binder placeholder; the caller MUST derive
46
- * the real binder over `message.subarray(0, truncatedLength)` and patch it in at
47
- * `binderOffset` before the hello touches the wire — a zero binder on the wire is a hello
48
- * every honest server must reject.
49
- * @property {(n: number) => Uint8Array} [randomBytes] injectable randomness
50
- */
51
- /**
52
- * The built hello plus everything later steps need to police the server's answer against what
53
- * was actually offered — negotiation checks must run against this record, never against the
54
- * defaults they might have come from.
55
- * @typedef {object} ClientHello
56
- * @property {Uint8Array} message framed handshake message, ready for the record layer
57
- * @property {Uint8Array} clientRandom
58
- * @property {Uint8Array} legacySessionId
59
- * @property {number[]} offeredCiphers
60
- * @property {number[]} offeredGroups
61
- * @property {number[]} offeredSigSchemes
62
- * @property {Set<number>} offeredExtensions extension types present in the hello
63
- * @property {string[]} offeredAlpn
64
- * @property {number} [binderOffset] psk only: where the binder's bytes sit in `message`
65
- * @property {number} [truncatedLength] psk only: how many leading bytes of `message` the binder
66
- * transcript covers (RFC 8446 s4.2.11.2 truncation — everything except the binders list)
67
- */
68
- /**
69
- * Build a ClientHello. Returns the framed handshake message plus the metadata the rest of the
70
- * handshake needs to police the server's answer.
71
- *
72
- * @param {ClientHelloOptions} opts
73
- * @returns {ClientHello}
74
- */
75
- export function buildClientHello({ hostname, keyShares, random, legacySessionId, ciphers, groups, sigSchemes, alpn, versions, extraExtensions, psk, randomBytes, }: ClientHelloOptions): ClientHello;
31
+ export function buildClientHello({ hostname, keyShares, random, legacySessionId, ciphers, groups, sigSchemes, alpn, versions, extensionOrder, extraExtensions, psk, randomBytes, }: {
32
+ hostname: any;
33
+ keyShares: any;
34
+ random: any;
35
+ legacySessionId: any;
36
+ ciphers: any;
37
+ groups?: number[] | undefined;
38
+ sigSchemes?: number[] | undefined;
39
+ alpn?: string[] | undefined;
40
+ versions?: number[] | undefined;
41
+ extensionOrder?: readonly number[] | undefined;
42
+ extraExtensions?: never[] | undefined;
43
+ psk?: null | undefined;
44
+ randomBytes?: ((n: any) => Uint8Array<any>) | undefined;
45
+ }): {
46
+ message: Uint8Array<ArrayBufferLike>;
47
+ clientRandom: any;
48
+ legacySessionId: any;
49
+ offeredCiphers: any;
50
+ offeredGroups: number[];
51
+ offeredSigSchemes: number[];
52
+ offeredExtensions: Set<any>;
53
+ offeredAlpn: string[];
54
+ };
76
55
  /**
77
56
  * Patch the real binder over the placeholder `buildClientHello` emitted. Separate from the
78
57
  * builder because the binder is derived FROM the built message (truncated), so there is no
@@ -308,6 +287,71 @@ export function checkFinished(received: Uint8Array, expected: Uint8Array): true;
308
287
  * @returns {string | null} null when the server declined ALPN entirely
309
288
  */
310
289
  export function checkAlpn(extensions: Map<number, Uint8Array>, offeredAlpn: string[], where: string): string | null;
290
+ /**
291
+ * @typedef {object} ClientHelloOptions
292
+ * @property {string} hostname SNI, unless it is an IP literal (then no SNI is sent)
293
+ * @property {Array<{ group: number, keyExchange: Uint8Array }>} keyShares public halves to
294
+ * offer; empty for a 1.2-only hello, whose wire form must not carry the extension at all
295
+ * @property {Uint8Array} [random] fixed ClientHello.random, for reproducible handshakes
296
+ * @property {Uint8Array} [legacySessionId] fixed legacy_session_id, likewise
297
+ * @property {number[]} [ciphers] default: the union for the offered versions, 1.3 first
298
+ * @property {number[]} [groups] supported_groups, default SUPPORTED_GROUPS
299
+ * @property {number[]} [sigSchemes] default SUPPORTED_SIG_SCHEMES
300
+ * @property {string[]} [alpn] default ['http/1.1']; empty array omits the extension
301
+ * @property {number[]} [versions] default [TLS13, TLS12]
302
+ * @property {Uint8Array[]} [extraExtensions] pre-encoded, sent verbatim (the HRR cookie)
303
+ * @property {{ identity: Uint8Array, obfuscatedTicketAge: number, binderLen: number }} [psk]
304
+ * offer this resumption PSK. Encoded with a zeroed binder placeholder; the caller MUST derive
305
+ * the real binder over `message.subarray(0, truncatedLength)` and patch it in at
306
+ * `binderOffset` before the hello touches the wire — a zero binder on the wire is a hello
307
+ * every honest server must reject.
308
+ * @property {(n: number) => Uint8Array} [randomBytes] injectable randomness
309
+ */
310
+ /**
311
+ * The built hello plus everything later steps need to police the server's answer against what
312
+ * was actually offered — negotiation checks must run against this record, never against the
313
+ * defaults they might have come from.
314
+ * @typedef {object} ClientHello
315
+ * @property {Uint8Array} message framed handshake message, ready for the record layer
316
+ * @property {Uint8Array} clientRandom
317
+ * @property {Uint8Array} legacySessionId
318
+ * @property {number[]} offeredCiphers
319
+ * @property {number[]} offeredGroups
320
+ * @property {number[]} offeredSigSchemes
321
+ * @property {Set<number>} offeredExtensions extension types present in the hello
322
+ * @property {string[]} offeredAlpn
323
+ * @property {number} [binderOffset] psk only: where the binder's bytes sit in `message`
324
+ * @property {number} [truncatedLength] psk only: how many leading bytes of `message` the binder
325
+ * transcript covers (RFC 8446 s4.2.11.2 truncation — everything except the binders list)
326
+ */
327
+ /**
328
+ * Build a ClientHello. Returns the framed handshake message plus the metadata the rest of the
329
+ * handshake needs to police the server's answer.
330
+ *
331
+ * @param {ClientHelloOptions} opts
332
+ * @returns {ClientHello}
333
+ */
334
+ /**
335
+ * Extension emission order, by type. This is not cosmetic: JA3 and JA4 hash the extension list in
336
+ * WIRE ORDER, so the order alone is a large part of what a fingerprinter reads.
337
+ *
338
+ * Captured from curl 8.21.0 / OpenSSL 3.6.3, which sends:
339
+ * renegotiation_info, server_name, ec_point_formats, supported_groups, ALPN, encrypt_then_mac,
340
+ * extended_master_secret, post_handshake_auth, signature_algorithms, supported_versions,
341
+ * psk_key_exchange_modes, key_share
342
+ *
343
+ * Two of those this package does not send, and the reason is the same in both cases — an extension
344
+ * is a claim about what we can do. encrypt_then_mac only applies to CBC suites, which are not
345
+ * offered; post_handshake_auth invites a CertificateRequest after the handshake, which is not
346
+ * implemented. status_request goes the other way: curl does not send it, this package does,
347
+ * because a stapled OCSP response is its only revocation signal. It is placed where OpenSSL puts
348
+ * it when it does send one, right after server_name.
349
+ *
350
+ * Anything not named here keeps its natural position at the end, and pre_shared_key is forced last
351
+ * whatever the caller asks for, because RFC 8446 s4.2.11 defines the binder transcript as the hello
352
+ * truncated just before the binders — a range that only exists if nothing follows them.
353
+ */
354
+ export const CURL_EXTENSION_ORDER: readonly number[];
311
355
  export { GROUP_PARAMS };
312
356
  /**
313
357
  * An ephemeral key share: the public half as sent in key_share, plus the private key the
@@ -324,6 +368,18 @@ export type KeyShare = {
324
368
  */
325
369
  privateKey: CryptoKey;
326
370
  };
371
+ /**
372
+ * A parsed ServerHello. `isHelloRetryRequest` is decided by the random alone (RFC 8446 s4.1.3);
373
+ * everything else is exactly what the wire carried, judged later by the negotiate* functions.
374
+ */
375
+ export type ServerHello = {
376
+ legacyVersion: number;
377
+ random: Uint8Array;
378
+ legacySessionIdEcho: Uint8Array;
379
+ cipherSuite: number;
380
+ extensions: Map<number, Uint8Array>;
381
+ isHelloRetryRequest: boolean;
382
+ };
327
383
  export type ClientHelloOptions = {
328
384
  /**
329
385
  * SNI, unless it is an IP literal (then no SNI is sent)
@@ -415,16 +471,4 @@ export type ClientHello = {
415
471
  */
416
472
  truncatedLength?: number | undefined;
417
473
  };
418
- /**
419
- * A parsed ServerHello. `isHelloRetryRequest` is decided by the random alone (RFC 8446 s4.1.3);
420
- * everything else is exactly what the wire carried, judged later by the negotiate* functions.
421
- */
422
- export type ServerHello = {
423
- legacyVersion: number;
424
- random: Uint8Array;
425
- legacySessionIdEcho: Uint8Array;
426
- cipherSuite: number;
427
- extensions: Map<number, Uint8Array>;
428
- isHelloRetryRequest: boolean;
429
- };
430
474
  import { GROUP_PARAMS } from './constants.js';