tunnelfetch 1.3.0 → 1.4.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
@@ -278,6 +278,32 @@ it saves do not pay it back — see [What this cannot do](#what-this-cannot-do-a
278
278
  are validated as HTTP tokens, a decoder that throws fails the body closed rather than truncating it,
279
279
  and an unregistered coding is still refused.
280
280
 
281
+ ### The Chrome identity, in one import
282
+
283
+ ```js
284
+ import { Client } from 'tunnelfetch';
285
+ import { chrome } from 'tunnelfetch/profile/chrome';
286
+
287
+ const client = new Client({ profile: chrome, connect, proxy, decoders: { br, zstd } });
288
+ ```
289
+
290
+ The subpath carries the two primitives this runtime has no native path for — ML-KEM-768 for the
291
+ `X25519MLKEM768` key exchange and ChaCha20-Poly1305 for the record layer, both compiled to
292
+ freestanding WASM and both with known-answer tests in this repository. **Importing it is the
293
+ opt-in:** a bundler pulls them in only for code on this path, so the default identity carries none
294
+ of it.
295
+
296
+ `br` and `zstd` stay yours. They are not cryptography and there is no single right implementation,
297
+ so the profile keeps refusing until you supply them — a Chrome that advertises `br` and cannot read
298
+ it is worse than one that says so.
299
+
300
+ Verified end to end, not merely constructed:
301
+
302
+ | Origin | | TLS | Group | HTTP |
303
+ |---|---|---|---|---|
304
+ | `blog.cloudflare.com` | 200 | 1.3 | `0x11ec` X25519MLKEM768 | h2 |
305
+ | `www.shopify.com` | 200 | 1.3 | `0x11ec` X25519MLKEM768 | h2 |
306
+
281
307
  ### HTTP/2 — access, not speed
282
308
 
283
309
  The client offers `h2` and `http/1.1` in ALPN by default and speaks whichever the server selects.
@@ -344,16 +370,19 @@ trades a fingerprint mismatch for a broken handshake, which is worse and fails s
344
370
 
345
371
  | curl sends | This package | Why |
346
372
  |---|---|---|
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 |
373
+ | 30 cipher suites, incl. ChaCha20, RSA key exchange and CBC | 6 AEAD suites, or 7 with ChaCha20 injected | RSA-kx and CBC are refused by design a server selecting `TLS_RSA_WITH_AES_256_CBC_SHA` would get a dead connection. `TLS_CHACHA20_POLY1305_SHA256` is injectable via `ciphers: { chacha20 }`: WebCrypto has no ChaCha20 here, so an implementation is supplied rather than a `node:crypto` dependency taken |
374
+ | `X25519MLKEM768` group and a 1216-byte key share | offered only when injected | ML-KEM is not a WebCrypto primitive; supply it as `groups: { x25519mlkem768 }` (what `profiles.chrome` requires) and the group and its 1216-byte hybrid key share go on the wire |
349
375
  | SHA-1 signature schemes | not offered | Refused deliberately |
350
376
  | `encrypt_then_mac` | not sent | Applies only to CBC suites, which are not offered |
351
377
  | `post_handshake_auth` | not sent | Invites a post-handshake `CertificateRequest`, which is not implemented |
352
378
  | — | `status_request` | curl does not ask for a stapled OCSP response; this package must, because a staple is its only revocation signal |
353
379
 
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.
380
+ ChaCha20-Poly1305 and X25519MLKEM768 are reachable by **injection**: an implementation of each is
381
+ supplied through `ciphers` / `groups`, which is precisely what `profiles.chrome` requires, and
382
+ neither is offered unless one is a suite or group advertised but not performable is a dead
383
+ connection if a server takes it. RSA key exchange and CBC suites stay refused on purpose;
384
+ `tls.ciphers` and `tls.groups` will let you offer them anyway, and the handshake will then fail if a
385
+ server picks one, which is yours to own.
357
386
 
358
387
  A test asserts this delta is exactly the list above, so gaining one of these capabilities without
359
388
  updating the table fails the build.
@@ -381,7 +410,9 @@ res.tunnelfetch.httpVersion; // '2' if the server chose h2, '1.1' otherwise
381
410
  | `connect` | — | Socket factory. On Workers, the `connect` export of `cloudflare:sockets`. Required for anything the platform's `fetch` cannot serve. |
382
411
  | `proxy` | `null` | URL string or object. `http:`, `https:`, `socks5:`, `socks5h:`. |
383
412
  | `trust` | `{mode:'system'}` | Certificate policy; see below. |
384
- | `tls` | `{}` | Handshake options (`alpn`, `groups`, `ciphers`, `offerGroups`). |
413
+ | `tls` | `{}` | Handshake options (`alpn`, `groups`, `ciphers`, `offerGroups`). Here `groups`/`ciphers` are number lists — the suite and group ids to offer, in preference order. |
414
+ | `ciphers` | `{}` | Injected AEAD implementations by capability name: `{ chacha20 }` (a `seal`/`open` pair). WebCrypto has no ChaCha20 here, so `TLS_CHACHA20_POLY1305_SHA256` is offered only when this is supplied. Required by `profiles.chrome`. |
415
+ | `groups` | `{}` | Injected key-exchange implementations by capability name: `{ x25519mlkem768 }` (ML-KEM-768 `keygen`/`encapsulate`/`decapsulate`). ML-KEM is not a WebCrypto primitive, so the post-quantum hybrid group is offered only when this is supplied. Required by `profiles.chrome`. |
385
416
  | `timeouts` | see below | `connectMs`, `handshakeMs`, `headersMs`, `idleMs`, `totalMs`. |
386
417
  | `cookies` | `false` | Enable a per-Client cookie jar. |
387
418
  | `maxRedirects` | `20` | |
@@ -493,9 +524,15 @@ Not implemented, and not planned:
493
524
  since 2020. Note the distinction: a server that *also* supports 1.0/1.1 is fine, because we will
494
525
  negotiate 1.2 or 1.3 with it. Only a server that supports *nothing else* is out of reach.
495
526
  - **RSA key transport.** No forward secrecy.
496
- - **ChaCha20-Poly1305.** It buys nothing: a server can only pick a suite we offered, TLS 1.3
497
- mandates AES-128-GCM, and AES-GCM is universal in TLS 1.2 deployments. WebCrypto has no
498
- ChaCha20, so offering it would mean a `node:crypto` dependency for zero compatibility gain.
527
+ - **ChaCha20-Poly1305, unless injected.** WebCrypto has no ChaCha20 on this runtime, so it is not
528
+ built in taking a `node:crypto` dependency would cost the package its "web platform only"
529
+ property. It is *injectable*, though: pass `ciphers: { chacha20 }` (a `seal`/`open` pair, e.g. a
530
+ WASM build) and `TLS_CHACHA20_POLY1305_SHA256` is offered — in curl's captured position, second
531
+ after AES-256-GCM — and used. Not built in because a server can only pick a suite we offered, TLS
532
+ 1.3 mandates AES-128-GCM, and AES-GCM is universal in TLS 1.2; the reason to add it is matching a
533
+ browser that offers it (`profiles.chrome`), not compatibility. The post-quantum
534
+ **X25519MLKEM768** group is injectable the same way — `groups: { x25519mlkem768 }` — for the same
535
+ reason: ML-KEM is not a WebCrypto primitive here.
499
536
  - **Client certificates (mTLS), 0-RTT, renegotiation.** A `HelloRequest` is refused rather than
500
537
  honoured. 0-RTT is a decision rather than an omission: early data can be replayed, so offering
501
538
  it would let an attacker who captured a POST replay it. (Session resumption itself *is*
@@ -565,25 +602,28 @@ Fetching a size-controlled origin through a proxy, warm, medians over seven-plus
565
602
  isolate, gzip on the wire. The last column is the same numbers as a rate, which is the form worth
566
603
  carrying around:
567
604
 
568
- | | New connection | Each further request, same connection | Marginal rate |
569
- | --- | --- | --- | --- |
570
- | 1 KB body | 11 ms | 2–3 ms | — |
571
- | 16 KB body | 11 ms | 3 ms | — |
572
- | 64 KB body | 11 ms | 2–4 ms | — |
573
- | 256 KB body | 7 ms | 3.4 ms | ~13 ms/MB |
574
- | 1 MB body | 11 ms | 6–12 ms | ~9 ms/MB |
575
- | 4 MB body | 31 ms | 21–35 ms | ~7 ms/MB |
576
- | **Same 16 KB page over HTTP/2** | 12 ms | 2.2 ms | — |
577
- | **First request in a fresh isolate** | 46 ms | — | — |
578
- | **…after `warmup({ iterations: 5 })`** | 16 ms | — | — |
579
-
580
- One model fits every body-size row to within its spread:
581
-
582
- > **≈ 9.5 ms to open a connection + 2 ms per request + 5–8 ms per MB of body**
583
-
584
- The connection term recovered independently from each row lands between 5 and 11 ms, agreeing with
585
- the 9–12 ms measured for a new connection by other means. Most of it is the TLS handshake and
586
- certificate chain validation; almost none of it is parsing (see below).
605
+ | | New connection (first request included) | Each further request, same connection |
606
+ | --- | --- | --- |
607
+ | 1 KB body | 6.4 ms | 1.6 ms |
608
+ | 16 KB body | 6.5 ms | 1.7 ms |
609
+ | 64 KB body | 6.7 ms | 1.9 ms |
610
+ | 256 KB body | 7.4 ms | 2.6 ms |
611
+ | 1 MB body | 10.4 ms | 5.6 ms |
612
+ | 4 MB body | 22.4 ms | 17.6 ms |
613
+ | **First request in a fresh isolate** | 46 ms | — |
614
+ | **…after `warmup({ iterations: 5 })`** | 16 ms | — |
615
+
616
+ Re-measured for 1.4.0 against a size-controlled origin through a proxy, ten rounds per size, HTTP/2
617
+ negotiated, gzip on the wire. The rows are not independent measurements — they are one model, fitted
618
+ to the sweep and then checked back against it:
619
+
620
+ > **≈ 6.4 ms to open a connection + 1.6 ms per further request + ~4 ms per MB of body**
621
+
622
+ Each sweep request fetches five pages — one on a fresh connection and four reusing it so the two
623
+ terms were separated by varying the reuse count rather than assumed: two pages against ten gives
624
+ 1.63 ms per further request, and the connection term falls out of the remainder. Fitted on 1 KB and
625
+ 4 MB, the model predicts 32.9 ms for the 1 MB row against 35 measured, and 92.9 for the 4 MB row
626
+ against 94.
587
627
 
588
628
  The ranges are real, not imprecision: absolute CPU on this platform varies by up to ~1.5× between
589
629
  isolates and runs — the same sweep repeated lands on faster and slower machines — so the values are
@@ -609,6 +649,31 @@ excess above the warm floor without `warmup()`, 15 ms with it at five iterations
609
649
  1.1 ms per request respectively, amortised over an isolate's early life. Warming costs 10 ms of
610
650
  startup at one iteration and 22 ms at five, against a 1 s budget, and does not lower the warm floor.
611
651
 
652
+ ### What the optional switches cost
653
+
654
+ Everything above is the default identity: curl's fingerprint, gzip and deflate, no post-quantum, no
655
+ GREASE. Each switch below is off unless asked for, and the table is what asking costs. Measured on
656
+ the edge the same way as the rest — differencing two work counts, minimum of samples.
657
+
658
+ | Switch | Cost | Paid |
659
+ |---|---|---|
660
+ | `grease: true` | not measurable | a handful of extra bytes in one hello |
661
+ | `tls.extensionOrder: 'shuffle'` | not measurable | shuffling ~11 items, once per handshake |
662
+ | `headerOrder` | not measurable — the ordered list is *faster* than the platform `Headers` (1.6 µs against 3.8 µs) | per request |
663
+ | `groups: { x25519mlkem768 }` | **+0.15 ms** with the bundled WASM, **+1.35 ms** with a pure-JS ML-KEM | per **connection**, not per request — amortised across every request that reuses it |
664
+ | `ciphers: { chacha20 }` | **+2.0 ms/MB**, and only if the server *selects* it | per byte. Servers with AES hardware generally prefer AES-GCM, so the usual cost is zero and the offer is what matters |
665
+ | `decoders: { br }` | **+4.4 ms/MB** | per byte, whenever an origin serves brotli. Harder compression is worse, not better: quality 11 is 16% smaller on the wire and 46% dearer to decode |
666
+ | `profile: chrome` | the sum of the three above | |
667
+
668
+ Two defaults moved in 1.4.0 and neither is visible in the table above them: matching curl's cipher
669
+ order means AES-256-GCM is negotiated where AES-128-GCM used to be, measured at **+4%** per MB
670
+ (1.50 against 1.45 ms/MB — hardware AES makes the extra rounds cheap), and the ordered header list
671
+ replaced the platform `Headers`, which is slightly *cheaper*. Both are inside the ±1.5× spread the
672
+ figures above already carry.
673
+
674
+ Bundle: importing `tunnelfetch/profile/chrome` adds **~22 KB gzipped** for the two WASM primitives.
675
+ Nothing else imports them, so a caller on the default identity carries none of it.
676
+
612
677
  ### What that costs in dollars
613
678
 
614
679
  Workers Standard bills $5/month including 10 million requests and 30 million CPU milliseconds, then
package/README.zh-CN.md CHANGED
@@ -219,6 +219,26 @@ const client = new Client({ connect, proxy, decoders: { br: brotli } });
219
219
 
220
220
  brotli 本身落在原生 inflate 的 1.9 倍。这个差价就是这个编码的成本,而它省下的线上字节买不回来——见[这个包做不到什么](#这个包做不到什么为什么)。解码器名字会按 HTTP token 校验;解码器抛错时响应体 fail closed,而不是被截断;未注册的编码依然会被拒绝。
221
221
 
222
+ ### 一行 import 得到 Chrome 身份
223
+
224
+ ```js
225
+ import { Client } from 'tunnelfetch';
226
+ import { chrome } from 'tunnelfetch/profile/chrome';
227
+
228
+ const client = new Client({ profile: chrome, connect, proxy, decoders: { br, zstd } });
229
+ ```
230
+
231
+ 这个子路径带着运行时没有原生实现的两个原语——`X25519MLKEM768` 密钥交换要的 ML-KEM-768,以及记录层要的 ChaCha20-Poly1305,都编译成无依赖的 WASM,并且都在本仓库里有已知答案测试。**导入即 opt-in**:打包器只为走这条路径的代码拉入它们,默认身份一个字节都不背。
232
+
233
+ `br` 和 `zstd` 仍然要你自己带。它们不是密码学、没有唯一正解,所以 profile 会一直拒绝到你提供为止——**一个声称支持 br 却读不了的 Chrome,比一个老实说不支持的更糟**。
234
+
235
+ 不是「能构造出来」,是端到端验证过:
236
+
237
+ | 源站 | | TLS | 群 | HTTP |
238
+ |---|---|---|---|---|
239
+ | `blog.cloudflare.com` | 200 | 1.3 | `0x11ec` X25519MLKEM768 | h2 |
240
+ | `www.shopify.com` | 200 | 1.3 | `0x11ec` X25519MLKEM768 | h2 |
241
+
222
242
  ### HTTP/2 — 要的是访问,不是速度
223
243
 
224
244
  客户端默认在 ALPN 中同时报出 `h2` 与 `http/1.1`,服务器选中哪个就说哪个。没有单独的 API:
@@ -410,17 +430,22 @@ CertificateError [CERT_PIN_MISMATCH]: no certificate in the chain matches any co
410
430
  通过代理抓取一个尺寸可控的源站,热态,同一 isolate 上 7 轮以上取中位数,传输走 gzip。最后一列是同样的数字
411
431
  换算成速率,那是更值得随身记住的形式:
412
432
 
413
- | | 新连接 | 同一连接上的后续请求 | 边际速率 |
414
- | --- | --- | --- | --- |
415
- | 1 KB body | 11 ms | 2–3 ms | — |
416
- | 16 KB body | 11 ms | 3 ms | — |
417
- | 64 KB body | 11 ms | 2–4 ms | — |
418
- | 256 KB body | 7 ms | 3.4 ms | ~13 ms/MB |
419
- | 1 MB body | 11 ms | 6–12 ms | ~9 ms/MB |
420
- | 4 MB body | 31 ms | 21–35 ms | ~7 ms/MB |
421
- | **同一个 16 KB 页面走 HTTP/2** | 12 ms | 2.2 ms | — |
422
- | **全新 isolate 的第一个请求** | 46 ms | | — |
423
- | **……调用 `warmup({ iterations: 5 })` 之后** | 16 ms | — | — |
433
+ | | 新连接(含首个请求) | 同连接每次后续请求 |
434
+ | --- | --- | --- |
435
+ | 1 KB body | 6.4 ms | 1.6 ms |
436
+ | 16 KB body | 6.5 ms | 1.7 ms |
437
+ | 64 KB body | 6.7 ms | 1.9 ms |
438
+ | 256 KB body | 7.4 ms | 2.6 ms |
439
+ | 1 MB body | 10.4 ms | 5.6 ms |
440
+ | 4 MB body | 22.4 ms | 17.6 ms |
441
+ | **全新 isolate 的第一个请求** | 46 ms | — |
442
+ | **…执行 `warmup({ iterations: 5 })` 之后** | 16 ms | — |
443
+
444
+ 为 1.4.0 重新测量:经代理打一个尺寸可控的源站,每个尺寸十轮,协商 HTTP/2,线上走 gzip。这些行**不是各自独立的测量**,而是一个模型拟合出来再回代验证的:
445
+
446
+ > **≈ 开一条连接 6.4 ms + 每次后续请求 1.6 ms + 每 MB body 约 4 ms**
447
+
448
+ 每次扫描请求会抓五个页面——一个走新连接、四个复用它——所以两项是靠**改变复用次数**分离出来的,不是假设的:两页对十页得出每次后续请求 1.63 ms,连接项由余数得到。用 1 KB 和 4 MB 拟合,模型预测 1 MB 那行 32.9 ms(实测 35)、4 MB 那行 92.9 ms(实测 94)。
424
449
 
425
450
  有一个模型能把每一个尺寸行都拟合到它的散布之内:
426
451
 
@@ -517,6 +542,24 @@ inflate 本身(约 2 ms/MB)加上把 body 物化成 JS 字符串(约 1.7 m
517
542
  导入这个包是免费的。121 个内置锚是以主题 DN 哈希为索引的 base64 字符串,只有链实际落到的那一个会被解码,所以
518
543
  380 KB 打包(gzip 后 133 KB)的启动时间保持在约 2 ms,而一个导入了但没使用本包的请求是 0 ms。
519
544
 
545
+ ### 可选开关各自的成本
546
+
547
+ 上面所有数字都是默认身份:curl 指纹、gzip 与 deflate、无后量子、无 GREASE。下面每个开关默认都关着,这张表是「打开它要付什么」。测法与其余部分一致——差分两个工作量、取样本最小值。
548
+
549
+ | 开关 | 成本 | 何时付 |
550
+ |---|---|---|
551
+ | `grease: true` | 测不出来 | 一次 hello 里多几个字节 |
552
+ | `tls.extensionOrder: 'shuffle'` | 测不出来 | 每次握手洗牌约 11 个元素 |
553
+ | `headerOrder` | 测不出来——有序列表比平台的 `Headers` **更快**(1.6 µs 对 3.8 µs) | 每请求 |
554
+ | `groups: { x25519mlkem768 }` | 用内置 WASM **+0.15 ms**,用纯 JS 的 ML-KEM **+1.35 ms** | **每连接**,不是每请求——复用该连接的所有请求共同摊薄 |
555
+ | `ciphers: { chacha20 }` | **+2.0 ms/MB**,而且只有服务器**选中**它时才付 | 每字节。有 AES 硬件加速的服务器通常偏好 AES-GCM,所以实际成本往往是零,真正起作用的是"出现在 offer 里" |
556
+ | `decoders: { br }` | **+4.4 ms/MB** | 每字节,只要源站发 brotli。压得越狠越亏:quality 11 线上小 16%,解码贵 46% |
557
+ | `profile: chrome` | 上面三项之和 | |
558
+
559
+ 1.4.0 里有两个默认值变了,而它们都没有出现在上面那张表里:套件顺序对齐 curl 之后,原本协商 AES-128-GCM 的地方现在协商 AES-256-GCM,实测**贵 4%**(1.50 对 1.45 ms/MB——硬件 AES 让多出来的轮次很便宜);有序 header 列表取代了平台 `Headers`,反而**略微更便宜**。两者都落在上面数字本来就带的 ±1.5× 波动之内。
560
+
561
+ 体积:`import 'tunnelfetch/profile/chrome'` 会为两个 WASM 原语增加约 **22 KB(gzip)**。没有其它地方引用它们,所以走默认身份的使用者一个字节都不背。
562
+
520
563
  ### 计划限制
521
564
 
522
565
  付费计划上 30 秒的默认 CPU 上限不是约束点——那大约是一次调用里 3000 条连接或 1 GB 的 body,而"同时等待响应头
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tunnelfetch",
3
- "version": "1.3.0",
3
+ "version": "1.4.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",
@@ -51,6 +51,10 @@
51
51
  "./roots": {
52
52
  "types": "./types/trust/roots.d.ts",
53
53
  "default": "./src/trust/roots.js"
54
+ },
55
+ "./profile/chrome": {
56
+ "types": "./types/profile/chrome.d.ts",
57
+ "default": "./src/profile/chrome.js"
54
58
  }
55
59
  },
56
60
  "types": "./types/index.d.ts",
package/src/client.js CHANGED
@@ -83,6 +83,15 @@ const NULL_BODY_STATUS = new Set([101, 204, 205, 304]);
83
83
  * yours and visible. Measured on the edge: WASM brotli decodes at about 2x native gzip, and
84
84
  * the wire bytes it saves do not pay that back — see the README. The reason to turn it on is
85
85
  * matching a browser's Accept-Encoding, not saving CPU.
86
+ * @property {{ chacha20?: import('./tls/aead.js').AeadOptions['impl'] }} [ciphers] injected AEAD
87
+ * implementations, by capability name. `chacha20` (seal/open, RFC 8439) is what lets
88
+ * TLS_CHACHA20_POLY1305_SHA256 be offered and performed — WebCrypto has no ChaCha20 on this
89
+ * runtime, and a suite offered but not performable is a dead connection if a server selects it,
90
+ * so without this the suite stays out of the ClientHello. Required by `profiles.chrome`.
91
+ * @property {{ x25519mlkem768?: import('./tls/hybrid.js').MlKem768 }} [groups] injected key-exchange
92
+ * implementations, by capability name. `x25519mlkem768` (ML-KEM-768 keygen/encapsulate/
93
+ * decapsulate) is what lets the post-quantum hybrid group be offered and performed; without it
94
+ * the group stays out of the ClientHello. Required by `profiles.chrome`.
86
95
  * @property {boolean} [keepAlive] default true.
87
96
  * @property {import('./profiles.js').FingerprintProfile} [profile] one coherent network identity
88
97
  * instead of a dozen knobs that can disagree — TLS, HTTP/2, header order and default headers
@@ -403,7 +412,7 @@ async function openFreshAndSend(client, current, { hop, key, proxy, trust, tls }
403
412
  tls,
404
413
  alpn,
405
414
  resumption: resumptionFor(client, key),
406
- deps: o.deps,
415
+ deps: tlsDeps(o),
407
416
  deadlines,
408
417
  limits: o.limits ?? {},
409
418
  now: o.now,
@@ -597,6 +606,22 @@ function serverNeverSawIt(err) {
597
606
  return err instanceof TunnelFetchError && err.code === codes.TLS_TRUNCATED && err.detail?.got === 0;
598
607
  }
599
608
 
609
+ /**
610
+ * The TLS deps for a connection: the caller's injectable randomness/keygen, plus the injected
611
+ * crypto implementations (`ciphers` -> `aead`, `groups` -> `kem`) the TLS layer needs to offer and
612
+ * perform the capability-gated ChaCha20 suite and X25519MLKEM768 group. Folded in here rather than
613
+ * carried in `tls`, so they neither enter the pool key nor disable native-fetch delegation — they
614
+ * are injected primitives, not fingerprint configuration.
615
+ * @param {Readonly<import('./client.js').ClientOptions>} o
616
+ * @returns {import('./tls/connect.js').TlsDeps}
617
+ */
618
+ function tlsDeps(o) {
619
+ const deps = { ...(o.deps ?? {}) };
620
+ if (o.ciphers) deps.aead = o.ciphers;
621
+ if (o.groups) deps.kem = o.groups;
622
+ return deps;
623
+ }
624
+
600
625
  async function sendAndReceive(client, conn, current, { key, deadlines, reused }) {
601
626
  const o = client.options;
602
627
  const target = targetFromUrl(current.url);
@@ -638,7 +663,7 @@ async function sendAndReceive(client, conn, current, { key, deadlines, reused })
638
663
  trust: o.trust ?? { mode: 'system' },
639
664
  tls: o.tls ?? {},
640
665
  resumption: resumptionFor(client, key),
641
- deps: o.deps,
666
+ deps: tlsDeps(o),
642
667
  deadlines,
643
668
  limits: o.limits ?? {},
644
669
  now: o.now,
@@ -0,0 +1,45 @@
1
+ // The Chrome identity, ready to use. Importing this module IS the opt-in.
2
+ //
3
+ // `profiles.chrome` in the main entry is a declaration: it names what the identity needs and
4
+ // REFUSES to be presented without it, so nobody accidentally ships a Chromium ClientHello over a
5
+ // curl handshake. That refusal is correct but it leaves the caller to find and wire two WASM
6
+ // primitives, which is friction for the common case of "I want Chrome".
7
+ //
8
+ // This module closes that gap without moving the cost onto anyone else. The main entry stays free
9
+ // of both blobs — a bundler only pulls them in for code that imports THIS path, so a caller using
10
+ // the curl default pays nothing. That is the whole reason it is a separate entry point rather than
11
+ // a flag.
12
+ //
13
+ // import { Client } from 'tunnelfetch';
14
+ // import { chrome } from 'tunnelfetch/profile/chrome';
15
+ // new Client({ profile: chrome, connect, proxy });
16
+ //
17
+ // Still not supplied here: `br` and `zstd` decoders. Those are not cryptography and there is no
18
+ // single right implementation — bring your own through `decoders` (see the README). The profile
19
+ // will keep refusing until you do, which is the point.
20
+
21
+ import { chrome as declaration } from '../profiles.js';
22
+ import { chacha20poly1305 } from './vendor/chacha20poly1305.js';
23
+ import { mlkem768 } from './vendor/mlkem768.js';
24
+
25
+ /**
26
+ * `profiles.chrome` with the two capabilities this package cannot perform natively already wired
27
+ * in. Everything else about it — the cipher list, the groups, the shuffled extension order, GREASE,
28
+ * the HTTP/2 SETTINGS and pseudo-header order, the request header order — comes from the
29
+ * declaration unchanged, and all of it was captured off the wire from Chrome 150.
30
+ *
31
+ * @type {import('../profiles.js').FingerprintProfile & {
32
+ * ciphers: Record<string, unknown>, groups: Record<string, unknown> }}
33
+ */
34
+ export const chrome = Object.freeze({
35
+ ...declaration,
36
+ name: `${declaration.name} (with bundled ML-KEM and ChaCha20)`,
37
+ // These satisfy two of the four entries in `requires`. `decoder:br` and `decoder:zstd` remain the
38
+ // caller's, so constructing a Client with this profile still fails until they are supplied —
39
+ // deliberately, because a Chrome that advertises `br` and cannot read it is worse than one that
40
+ // says so up front.
41
+ ciphers: Object.freeze({ chacha20: chacha20poly1305 }),
42
+ groups: Object.freeze({ x25519mlkem768: mlkem768 }),
43
+ });
44
+
45
+ export { chacha20poly1305, mlkem768 };
@@ -0,0 +1,65 @@
1
+ // ChaCha20-Poly1305 (RFC 8439 IETF AEAD) — libsodium reference C (DJB chacha-merged +
2
+ // poly1305-donna32 + the _ietf_encrypt/decrypt_detached construction) compiled to
3
+ // freestanding wasm32 with wasi-sdk 25 / LLVM 19. Zero wasm imports, no WASI, no
4
+ // allocator. Sources and build: wasmcrypto/c/chacha20poly1305.c, wasmcrypto/build.sh.
5
+ // AUTO-GENERATED by wasmcrypto/gen-js.mjs — edit that, not this.
6
+ // wasm sha256[0..12): ec15f83a678a raw 8839 bytes
7
+
8
+ const B64 = "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OgC+ASACQQA6AL8BIABBvxBqLQAAIQUgAEG+EGotAAAhAyAAQb0Qai0AACEGIABBvBBqLQAAIQcgAEG7EGotAAAhCCAAQboQai0AACEJIABBuRBqLQAAIQogAEG4EGotAAAhCyAAQbcQai0AACEMIABBthBqLQAAIQ0gAEG1EGotAAAhDiAAQbQQai0AACEPIABBsxBqLQAAIRAgAEGyEGotAAAhESAAQbEQai0AACAAQbAQai0AACEAIAItAA8hEyACLQAOIRQgAi0ADSEVIAItAAwhFiACLQALIRcgAi0ACiEYIAItAAkhGSACLQAIIRogAi0AByEbIAItAAYhHCACLQAFIR0gAi0ABCEeIAItAAMhHyACLQACISAgAi0AASACLQAAISIgAkEAOgAAIAJBADoAASACQQA6AAIgAkEAOgADIAJBADoABCACQQA6AAUgAkEAOgAGIAJBADoAByACQQA6AAggAkEAOgAJIAJBADoACiACQQA6AAsgAkEAOgAMIAJBADoADSACQQA6AA4gAkEAOgAPQX8hBHMgACAic3IgESAgc3IgECAfc3IgDyAec3IgDiAdc3IgDSAcc3IgDCAbc3IgCyAac3IgCiAZc3IgCSAYc3IgCCAXc3IgByAWc3IgBiAVc3IgAyAUc3IgBSATc3JBAWtBgAJxRQRAQcAQIAEQAAwBC0HAEEHAECAjQQEQCiABIQQLIAJBwAFqJAAgBAvOCQE3fyMAQUBqIQkgA0IAUgRAIAAoAjwhHSAAKAI4IR4gACgCNCESIAAoAjAhEyAAKAIsIR8gACgCKCEgIAAoAiQhISAAKAIgISIgACgCHCEjIAAoAhghJCAAKAIUISUgACgCECEmIAAoAgwhJyAAKAIIISggACgCBCEpIAAoAgAhKiAJQThqISwgCUEwaiEtIAlBKGohLiAJQSBqIS8gCUEYaiEwIAlBEGohMQNAAkAgA0I/VgRAIAIhBQwBCyAsQgA3AwAgLUIANwMAIC5CADcDACAvQgA3AwAgMEIANwMAIDFCADcDACAJQgA3AwggCUIANwMAQQAhBANAIAQgCWogASAEai0AADoAACADIARBAWoiBK1WDQALIAkiBSEBIAIhKwtBbCEWICohCCApIQogKCEOICchFCAmIQQgJSECICQhBiAjIQcgIiELICEhDyAgIQwgHSEQIB4h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";
9
+
10
+ function fromBase64(s) {
11
+ const bin = atob(s);
12
+ const u = new Uint8Array(bin.length);
13
+ for (let i = 0; i < u.length; i++) u[i] = bin.charCodeAt(i);
14
+ return u;
15
+ }
16
+
17
+ // Module scope: on workerd this runs at isolate startup, which is not billed as request CPU.
18
+ // SYNCHRONOUS compile+instantiate, deliberately: the async forms (WebAssembly.compile /
19
+ // .instantiate) return promises that do not settle during workerd's startup evaluation, and
20
+ // the deploy is rejected with error 10021 "Top-level await in module is unsettled" — measured,
21
+ // not assumed. The module has zero imports; it cannot touch anything outside its own memory.
22
+ const _instance = new WebAssembly.Instance(new WebAssembly.Module(fromBase64(B64)), {});
23
+ const x = _instance.exports;
24
+
25
+ export const buildId = "ec15f83a678a";
26
+
27
+
28
+ const MSG = x.cc_msg(), AAD = x.cc_aad(), KEY = x.cc_key(), NONCE = x.cc_nonce();
29
+ const MSG_CAP = x.cc_msg_cap(), AAD_CAP = x.cc_aad_cap();
30
+ let mem = new Uint8Array(x.memory.buffer);
31
+ const view = () => (mem.buffer === x.memory.buffer ? mem : (mem = new Uint8Array(x.memory.buffer)));
32
+ const EMPTY = new Uint8Array(0);
33
+
34
+ function load(key, nonce, data, aad) {
35
+ if (key.length !== 32) throw new RangeError('key must be 32 bytes');
36
+ if (nonce.length !== 12) throw new RangeError('nonce must be 12 bytes');
37
+ if (aad.length > AAD_CAP) throw new RangeError('aad too long');
38
+ const m = view();
39
+ m.set(key, KEY);
40
+ m.set(nonce, NONCE);
41
+ m.set(aad, AAD);
42
+ m.set(data, MSG);
43
+ }
44
+
45
+ export const chacha20poly1305 = {
46
+ /** seal(key32, nonce12, plaintext, aad?) -> ciphertext||tag (RFC 8439 AEAD) */
47
+ seal(key, nonce, plaintext, aad = EMPTY) {
48
+ if (plaintext.length > MSG_CAP) throw new RangeError('plaintext too long');
49
+ load(key, nonce, plaintext, aad);
50
+ const n = x.cc_seal(plaintext.length, aad.length);
51
+ if (n < 0) throw new RangeError('seal rejected input');
52
+ return view().slice(MSG, MSG + n);
53
+ },
54
+ /** open(key32, nonce12, ciphertextAndTag, aad?) -> plaintext; throws on auth failure */
55
+ open(key, nonce, ciphertext, aad = EMPTY) {
56
+ if (ciphertext.length < 16 || ciphertext.length > MSG_CAP + 16) {
57
+ throw new Error('chacha20poly1305: invalid ciphertext');
58
+ }
59
+ load(key, nonce, ciphertext, aad);
60
+ const n = x.cc_open(ciphertext.length, aad.length);
61
+ if (n < 0) throw new Error('chacha20poly1305: authentication failed');
62
+ return view().slice(MSG, MSG + n);
63
+ },
64
+ };
65
+