tunnelfetch 1.1.2 → 1.3.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.
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Read the caller's header names in the order and case they wrote them, BEFORE anything hands them
3
+ * to `Request`, which is where both are lost.
4
+ *
5
+ * Recovers what is recoverable and no more: an array of pairs or a plain object still carries the
6
+ * caller's order, a `Headers` or a `Request` does not — those were normalised before this package
7
+ * ever saw them, and there is nothing here to reconstruct.
8
+ *
9
+ * @param {RequestInfo | URL} input
10
+ * @param {RequestInit} [init]
11
+ * @returns {Array<[string, string]> | null} null when the caller's order was already gone
12
+ */
13
+ export function callerHeaderOrder(input: RequestInfo | URL, init?: RequestInit): Array<[string, string]> | null;
14
+ /**
15
+ * Default request header order, from curl 8.21.0 on the wire. `'*'` is where headers not named
16
+ * here go, in the order they were given — which is where curl puts the caller's own.
17
+ */
18
+ export const CURL_HEADER_ORDER: readonly string[];
19
+ /**
20
+ * An ordered, case-preserving header list.
21
+ *
22
+ * Deliberately not a `Headers` subclass and deliberately not backed by one: the whole point is to
23
+ * be the thing `Headers` is not. Lookup and mutation are case-insensitive, as HTTP requires;
24
+ * iteration returns names exactly as they were written, in the order they arrived.
25
+ */
26
+ export class OrderedHeaders {
27
+ /** @param {Headers | Iterable<[string, string]> | Record<string, string> | null} [init] */
28
+ constructor(init?: Headers | Iterable<[string, string]> | Record<string, string> | null);
29
+ /** @type {Array<[string, string]>} name as written, value */
30
+ _list: Array<[string, string]>;
31
+ _indexOf(name: any): number;
32
+ has(name: any): boolean;
33
+ /** Comma-joined when a field appears more than once, matching `Headers.get`. */
34
+ get(name: any): string | null;
35
+ /**
36
+ * Replace IN PLACE when the field is already present, so setting a value does not move a header
37
+ * to the end and silently reorder the request. A caller who wrote `User-Agent` first and then
38
+ * had it overwritten should still see it first.
39
+ */
40
+ set(name: any, value: any): void;
41
+ append(name: any, value: any): void;
42
+ delete(name: any): void;
43
+ /**
44
+ * Put the fields into `order`, which names lowercased header names and may contain `'*'` to mark
45
+ * where everything unnamed goes. Fields keep their relative order within each group, so a
46
+ * caller's own sequence survives.
47
+ *
48
+ * @param {readonly string[]} order
49
+ */
50
+ reorder(order: readonly string[]): void;
51
+ /** @returns {Array<[string, string]>} names as written, in order */
52
+ entries(): Array<[string, string]>;
53
+ /** Lowercased names, for HTTP/2 where RFC 9113 s8.2.1 requires them. */
54
+ lowercased(): string[][];
55
+ [Symbol.iterator](): ArrayIterator<[string, string]>;
56
+ }
package/types/client.d.ts CHANGED
@@ -65,6 +65,25 @@ 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 {import('./profiles.js').FingerprintProfile} [profile] one coherent network identity
69
+ * instead of a dozen knobs that can disagree — TLS, HTTP/2, header order and default headers
70
+ * together. Explicit options win over it. A profile that declares capabilities this package
71
+ * cannot perform is REFUSED rather than silently reduced: see `profiles.chrome`.
72
+ * @property {readonly string[]} [headerOrder] request header names, lowercased, in the order to
73
+ * emit them; `'*'` marks where headers not named go, in the order the caller gave them. Defaults
74
+ * to curl's (`CURL_HEADER_ORDER`). The platform `Headers` sorts alphabetically and lowercases, so
75
+ * without this a request goes out with `user-agent` last, which no real client does.
76
+ * @property {string[]} [http2PseudoHeaderOrder] request pseudo-headers in the order to emit them.
77
+ * Defaults to curl's. Any of the four omitted is appended rather than dropped: RFC 9113 s8.3.1
78
+ * makes all four mandatory, so a request missing one is malformed rather than merely unusual.
79
+ * @property {Record<string, 'incremental'|'without'|'never'>} [http2HpackIndexing] per-field HPACK
80
+ * indexing. Which fields enter the dynamic table is read by an Akamai-style h2 fingerprint.
81
+ * Defaults to curl's: everything incremental except `:path`.
82
+ * @property {Array<[number, number]>} [http2Settings] the HTTP/2 SETTINGS flight, as [id, value]
83
+ * pairs. Order is significant — an Akamai-style h2 fingerprint reads the ids in the order they
84
+ * are sent — so this replaces the flight rather than merging into it. Defaults to curl's. The
85
+ * TLS half of the fingerprint is configured through `tls` (`ciphers`, `groups`, `sigSchemes`,
86
+ * `alpn`, `versions`, `extensionOrder`).
68
87
  * @property {boolean} [http2] offer HTTP/2 via ALPN and speak it when the server selects it.
69
88
  * Default true. The goal is ACCESS, not speed — some sites treat HTTP/1.1 as a bot signal — and
70
89
  * on a CPU-billed runtime h2 costs MORE than h1 (HPACK is extra work). Set false to offer only
@@ -222,6 +241,40 @@ export type ClientOptions = {
222
241
  * default true.
223
242
  */
224
243
  keepAlive?: boolean | undefined;
244
+ /**
245
+ * one coherent network identity
246
+ * instead of a dozen knobs that can disagree — TLS, HTTP/2, header order and default headers
247
+ * together. Explicit options win over it. A profile that declares capabilities this package
248
+ * cannot perform is REFUSED rather than silently reduced: see `profiles.chrome`.
249
+ */
250
+ profile?: import("./profiles.js").FingerprintProfile | undefined;
251
+ /**
252
+ * request header names, lowercased, in the order to
253
+ * emit them; `'*'` marks where headers not named go, in the order the caller gave them. Defaults
254
+ * to curl's (`CURL_HEADER_ORDER`). The platform `Headers` sorts alphabetically and lowercases, so
255
+ * without this a request goes out with `user-agent` last, which no real client does.
256
+ */
257
+ headerOrder?: readonly string[] | undefined;
258
+ /**
259
+ * request pseudo-headers in the order to emit them.
260
+ * Defaults to curl's. Any of the four omitted is appended rather than dropped: RFC 9113 s8.3.1
261
+ * makes all four mandatory, so a request missing one is malformed rather than merely unusual.
262
+ */
263
+ http2PseudoHeaderOrder?: string[] | undefined;
264
+ /**
265
+ * per-field HPACK
266
+ * indexing. Which fields enter the dynamic table is read by an Akamai-style h2 fingerprint.
267
+ * Defaults to curl's: everything incremental except `:path`.
268
+ */
269
+ http2HpackIndexing?: Record<string, "without" | "incremental" | "never"> | undefined;
270
+ /**
271
+ * the HTTP/2 SETTINGS flight, as [id, value]
272
+ * pairs. Order is significant — an Akamai-style h2 fingerprint reads the ids in the order they
273
+ * are sent — so this replaces the flight rather than merging into it. Defaults to curl's. The
274
+ * TLS half of the fingerprint is configured through `tls` (`ciphers`, `groups`, `sigSchemes`,
275
+ * `alpn`, `versions`, `extensionOrder`).
276
+ */
277
+ http2Settings?: [number, number][] | undefined;
225
278
  /**
226
279
  * offer HTTP/2 via ALPN and speak it when the server selects it.
227
280
  * Default true. The goal is ACCESS, not speed — some sites treat HTTP/1.1 as a bot signal — and
@@ -13,7 +13,7 @@ export function buildRequestFields({ method, scheme, authority, path, headers }:
13
13
  authority: string;
14
14
  path: string;
15
15
  headers: Array<[string, string]>;
16
- }): import("./hpack.js").HpackField[];
16
+ }, opts?: {}): import("./hpack.js").HpackField[];
17
17
  /**
18
18
  * @typedef {ReadableStream<Uint8Array> & { completed: Promise<boolean>,
19
19
  * trailers: Promise<Headers | null> }} BodyStream
@@ -74,6 +74,10 @@ 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;
79
+ _pseudoHeaderOrder: string[] | null;
80
+ _hpackIndexing: Record<string, "without" | "incremental" | "never"> | null;
77
81
  _expectFirstSettings: boolean;
78
82
  _fatal: any;
79
83
  _goaway: {
@@ -252,6 +256,26 @@ export type Http2ConnectionOptions = {
252
256
  * self-protection cap on a decoded response header list.
253
257
  */
254
258
  maxHeaderListSize?: number | undefined;
259
+ /**
260
+ * request pseudo-headers in the order to emit them.
261
+ * Defaults to curl's `[':method', ':scheme', ':authority', ':path']`. Any of the four left out is
262
+ * appended rather than dropped — RFC 9113 s8.3.1 makes all four mandatory and a request missing
263
+ * one is malformed, which is not a fingerprint choice anyone should be able to make by accident.
264
+ */
265
+ pseudoHeaderOrder?: string[] | undefined;
266
+ /**
267
+ * per-field HPACK
268
+ * indexing. Which fields enter the dynamic table is part of the fingerprint. Defaults to curl's:
269
+ * everything incremental except `:path`, which is sent without indexing.
270
+ */
271
+ hpackIndexing?: Record<string, "without" | "incremental" | "never"> | undefined;
272
+ /**
273
+ * the SETTINGS flight sent in the connection
274
+ * preface, as [id, value] pairs. Order is significant — an Akamai-style HTTP/2 fingerprint reads
275
+ * the ids in the order they are sent — so this replaces the flight entirely rather than merging.
276
+ * Defaults to curl's: MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, ENABLE_PUSH.
277
+ */
278
+ settings?: [number, number][] | undefined;
255
279
  /**
256
280
  * cap on the RAW bytes of one HEADERS+CONTINUATION run,
257
281
  * before HPACK decoding. Default 262144, matching the decoded cap. This is the bound that stops
@@ -79,7 +79,7 @@ export type HpackField = {
79
79
  * how to represent it when it is not a
80
80
  * full static match. Default 'incremental', which is what curl uses for most fields.
81
81
  */
82
- indexing?: "incremental" | "without" | "never" | undefined;
82
+ indexing?: "without" | "incremental" | "never" | undefined;
83
83
  };
84
84
  export type HpackDecoderOptions = {
85
85
  /**
package/types/index.d.ts CHANGED
@@ -15,3 +15,4 @@ export { openConnection, targetFromUrl, nativeFetchCanServe } from "./transport.
15
15
  export { openTunnel, parseProxy } from "./proxy/index.js";
16
16
  export { verifyChain, rootStoreProvenance } from "./trust/index.js";
17
17
  export { TunnelFetchError, ProxyError, HttpError, TlsError, TlsUnsupportedError, Http2Error, CertificateError, TimeoutError, LimitError, ConfigError, codes } from "./errors.js";
18
+ export { profiles, curl, chrome, applyProfile } from "./profiles.js";
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Fold a profile into a Client's options, and refuse an identity that cannot be honoured.
3
+ *
4
+ * Explicit options WIN over the profile: a caller who names a field meant to name it, and silently
5
+ * overriding them would make the profile impossible to adjust. The profile fills what was not said.
6
+ *
7
+ * @param {object} options as given to the Client
8
+ * @returns {object} options with the profile folded in
9
+ */
10
+ export function applyProfile(options: object): object;
11
+ /**
12
+ * @typedef {object} FingerprintProfile
13
+ * @property {string} name
14
+ * @property {object} [tls] merged into `tls`
15
+ * @property {readonly string[]} [headerOrder]
16
+ * @property {Array<[number, number]>} [http2Settings]
17
+ * @property {string[]} [http2PseudoHeaderOrder]
18
+ * @property {Record<string, string>} [http2HpackIndexing]
19
+ * @property {Array<[string, string]>} [headers] default request headers, in order
20
+ * @property {string[]} [requires] capabilities the caller must inject for this identity to be
21
+ * honest: `'cipher:chacha20'`, `'group:x25519mlkem768'`, `'decoder:br'`, `'decoder:zstd'`
22
+ */
23
+ /**
24
+ * curl 8.21.0 / OpenSSL 3.6.3. Complete: every layer was captured, and everything it offers is
25
+ * something this package can actually perform. This is the default identity.
26
+ */
27
+ export const curl: Readonly<{
28
+ name: "curl/8.21.0";
29
+ tls: Readonly<{
30
+ alpn: string[];
31
+ extensionOrder: readonly number[];
32
+ grease: false;
33
+ }>;
34
+ headerOrder: readonly string[];
35
+ headers: readonly string[][];
36
+ http2Settings: readonly number[][];
37
+ http2PseudoHeaderOrder: readonly string[];
38
+ http2HpackIndexing: Readonly<{
39
+ ':path': "without";
40
+ }>;
41
+ requires: readonly never[];
42
+ }>;
43
+ /**
44
+ * Chromium, TLS layer captured off the wire.
45
+ *
46
+ * INCOMPLETE ON PURPOSE, and it refuses to be used as though it were not. Two things are missing
47
+ * and neither can be papered over:
48
+ *
49
+ * * Chromium offers TLS_CHACHA20_POLY1305_SHA256 and the X25519MLKEM768 group, and this package
50
+ * implements neither. A ClientHello is an OFFER: a server may take either, and a client that
51
+ * then cannot complete the handshake has traded a fingerprint mismatch for a dead connection.
52
+ * Both are reachable by injection, which is why they are listed in `requires` rather than
53
+ * silently dropped.
54
+ * * Chromium's HTTP/2 preface was not captured — capturing it needs a TLS server the browser
55
+ * will trust, which is a different exercise. So this profile carries no h2 layer, and using it
56
+ * with HTTP/2 enabled would produce a Chromium ClientHello above a curl h2 preface: precisely
57
+ * the split identity a profile exists to prevent.
58
+ *
59
+ * `applyProfile` refuses both cases with a message naming what is missing.
60
+ */
61
+ export const chrome: Readonly<{
62
+ name: "chromium (TLS layer only)";
63
+ tls: Readonly<{
64
+ ciphers: readonly number[];
65
+ groups: readonly number[];
66
+ sigSchemes: readonly number[];
67
+ alpn: string[];
68
+ extensionOrder: "shuffle";
69
+ grease: true;
70
+ }>;
71
+ headers: readonly string[][];
72
+ http2Settings: null;
73
+ requires: readonly string[];
74
+ }>;
75
+ /** @type {Record<string, FingerprintProfile>} */
76
+ export const profiles: Record<string, FingerprintProfile>;
77
+ export type FingerprintProfile = {
78
+ name: string;
79
+ /**
80
+ * merged into `tls`
81
+ */
82
+ tls?: object | undefined;
83
+ headerOrder?: readonly string[] | undefined;
84
+ http2Settings?: [number, number][] | undefined;
85
+ http2PseudoHeaderOrder?: string[] | undefined;
86
+ http2HpackIndexing?: Record<string, string> | undefined;
87
+ /**
88
+ * default request headers, in order
89
+ */
90
+ headers?: [string, string][] | undefined;
91
+ /**
92
+ * capabilities the caller must inject for this identity to be
93
+ * honest: `'cipher:chacha20'`, `'group:x25519mlkem768'`, `'decoder:br'`, `'decoder:zstd'`
94
+ */
95
+ requires?: string[] | undefined;
96
+ };
@@ -14,6 +14,17 @@
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 {boolean | number} [grease] send GREASE (RFC 8701) reserved values in the cipher list,
19
+ * the extension list (one at each end), supported_groups, supported_versions and key_share.
20
+ * Default false, because curl does not GREASE — Chromium does. A number is a seed, which makes
21
+ * the hello reproducible; `true` draws one from `deps.randomBytes`. A server that negotiates a
22
+ * GREASE value is refused with a typed error naming it.
23
+ * @property {number[] | 'shuffle'} [extensionOrder] ClientHello extension types, in the order to emit them.
24
+ * JA3 and JA4 hash the extension list in WIRE ORDER, so this is most of what a fingerprinter
25
+ * reads. Defaults to curl's order (`CURL_EXTENSION_ORDER`). Extensions not named keep their
26
+ * natural position at the end; `pre_shared_key` is always last whatever is asked, because RFC
27
+ * 8446 s4.2.11 defines the binder transcript as the hello truncated just before the binders.
17
28
  * @property {Uint8Array} [clientRandom] fixed ClientHello.random, for reproducible handshakes.
18
29
  * @property {Uint8Array} [legacySessionId] fixed legacy_session_id, likewise.
19
30
  * @property {boolean} [compatibilityCcs] send the middlebox-compatibility ChangeCipherSpec.
@@ -144,6 +155,26 @@ export type TlsOptions = {
144
155
  * cipher suites to offer, in preference order.
145
156
  */
146
157
  ciphers?: number[] | undefined;
158
+ /**
159
+ * signature_algorithms to offer, in preference order.
160
+ */
161
+ sigSchemes?: number[] | undefined;
162
+ /**
163
+ * send GREASE (RFC 8701) reserved values in the cipher list,
164
+ * the extension list (one at each end), supported_groups, supported_versions and key_share.
165
+ * Default false, because curl does not GREASE — Chromium does. A number is a seed, which makes
166
+ * the hello reproducible; `true` draws one from `deps.randomBytes`. A server that negotiates a
167
+ * GREASE value is refused with a typed error naming it.
168
+ */
169
+ grease?: number | boolean | undefined;
170
+ /**
171
+ * ClientHello extension types, in the order to emit them.
172
+ * JA3 and JA4 hash the extension list in WIRE ORDER, so this is most of what a fingerprinter
173
+ * reads. Defaults to curl's order (`CURL_EXTENSION_ORDER`). Extensions not named keep their
174
+ * natural position at the end; `pre_shared_key` is always last whatever is asked, because RFC
175
+ * 8446 s4.2.11 defines the binder transcript as the hello truncated just before the binders.
176
+ */
177
+ extensionOrder?: number[] | "shuffle" | undefined;
147
178
  /**
148
179
  * fixed ClientHello.random, for reproducible handshakes.
149
180
  */
@@ -1,3 +1,10 @@
1
+ /**
2
+ * An extension of an arbitrary type with an arbitrary body. Exists for GREASE (RFC 8701), whose
3
+ * whole point is to carry a reserved type this package assigns no meaning to.
4
+ * @param {number} type
5
+ * @param {Uint8Array} body
6
+ */
7
+ export function encodeRawExtension(type: number, body: Uint8Array): Uint8Array<ArrayBufferLike>;
1
8
  /**
2
9
  * server_name (RFC 6066). Only host_name (type 0) exists in practice.
3
10
  * An IP literal must NOT be sent as SNI — RFC 6066 s3 forbids it, and servers that do virtual
@@ -0,0 +1,46 @@
1
+ /** @param {number} v @returns {boolean} */
2
+ export function isGrease(v: number): boolean;
3
+ /**
4
+ * A deterministic-from-seed source of GREASE values and shuffles.
5
+ *
6
+ * Seeded rather than ad-hoc `Math.random` for two reasons: this package forbids ambient randomness
7
+ * in `src/` (repo-hygiene enforces it, so that every byte on the wire is reproducible in a test),
8
+ * and a fingerprint that cannot be reproduced cannot be asserted byte-for-byte.
9
+ *
10
+ * @param {number} seed
11
+ */
12
+ export function greaseSource(seed: number): {
13
+ /** A GREASE value not yet handed out in this hello, since Chromium never repeats one. */
14
+ take(): number;
15
+ next: () => number;
16
+ };
17
+ /**
18
+ * Fisher-Yates over the middle of the extension list, leaving the ends alone.
19
+ *
20
+ * The first and last positions are not free: Chromium pins a GREASE extension to each, and
21
+ * `pre_shared_key` MUST be last of all (RFC 8446 s4.2.11 — the binder transcript is the hello
22
+ * truncated just before the binders, a range that only exists if nothing follows them). So the
23
+ * shuffle covers everything between the fixed ends and nothing else.
24
+ *
25
+ * @param {Array<Uint8Array>} parts encoded extensions, already ordered
26
+ * @param {{next: () => number}} rng
27
+ * @param {(e: Uint8Array) => number} typeOf
28
+ * @param {number} pskType
29
+ * @returns {Array<Uint8Array>}
30
+ */
31
+ export function shuffleExtensions(parts: Array<Uint8Array>, rng: {
32
+ next: () => number;
33
+ }, typeOf: (e: Uint8Array) => number, pskType: number): Array<Uint8Array>;
34
+ /**
35
+ * A GREASE key_share entry: a reserved group with a single-byte key, which is what Chromium sends.
36
+ * The byte is fixed rather than random — it is never used for anything, and a value that varies
37
+ * would only make the hello harder to assert on.
38
+ *
39
+ * @param {number} group
40
+ */
41
+ export function greaseKeyShare(group: number): {
42
+ group: number;
43
+ keyExchange: Uint8Array<ArrayBuffer>;
44
+ };
45
+ /** The sixteen reserved values (RFC 8701 s2): 0x0A0A, 0x1A1A, ... 0xFAFA. */
46
+ export const GREASE_VALUES: readonly number[];
@@ -28,51 +28,31 @@ 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, grease, 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
+ grease?: boolean | undefined;
45
+ randomBytes?: ((n: any) => Uint8Array<any>) | undefined;
46
+ }): {
47
+ message: Uint8Array<ArrayBufferLike>;
48
+ clientRandom: any;
49
+ legacySessionId: any;
50
+ offeredCiphers: any;
51
+ offeredGroups: number[];
52
+ offeredSigSchemes: number[];
53
+ offeredExtensions: Set<any>;
54
+ offeredAlpn: string[];
55
+ };
76
56
  /**
77
57
  * Patch the real binder over the placeholder `buildClientHello` emitted. Separate from the
78
58
  * builder because the binder is derived FROM the built message (truncated), so there is no
@@ -308,6 +288,73 @@ export function checkFinished(received: Uint8Array, expected: Uint8Array): true;
308
288
  * @returns {string | null} null when the server declined ALPN entirely
309
289
  */
310
290
  export function checkAlpn(extensions: Map<number, Uint8Array>, offeredAlpn: string[], where: string): string | null;
291
+ /**
292
+ * @typedef {object} ClientHelloOptions
293
+ * @property {string} hostname SNI, unless it is an IP literal (then no SNI is sent)
294
+ * @property {Array<{ group: number, keyExchange: Uint8Array }>} keyShares public halves to
295
+ * offer; empty for a 1.2-only hello, whose wire form must not carry the extension at all
296
+ * @property {Uint8Array} [random] fixed ClientHello.random, for reproducible handshakes
297
+ * @property {Uint8Array} [legacySessionId] fixed legacy_session_id, likewise
298
+ * @property {number[]} [ciphers] default: the union for the offered versions, 1.3 first
299
+ * @property {number[]} [groups] supported_groups, default SUPPORTED_GROUPS
300
+ * @property {number[]} [sigSchemes] default SUPPORTED_SIG_SCHEMES
301
+ * @property {string[]} [alpn] default ['http/1.1']; empty array omits the extension
302
+ * @property {number[]} [versions] default [TLS13, TLS12]
303
+ * @property {Uint8Array[]} [extraExtensions] pre-encoded, sent verbatim (the HRR cookie)
304
+ * @property {{ identity: Uint8Array, obfuscatedTicketAge: number, binderLen: number }} [psk]
305
+ * offer this resumption PSK. Encoded with a zeroed binder placeholder; the caller MUST derive
306
+ * the real binder over `message.subarray(0, truncatedLength)` and patch it in at
307
+ * `binderOffset` before the hello touches the wire — a zero binder on the wire is a hello
308
+ * every honest server must reject.
309
+ * @property {(n: number) => Uint8Array} [randomBytes] injectable randomness
310
+ */
311
+ /**
312
+ * The built hello plus everything later steps need to police the server's answer against what
313
+ * was actually offered — negotiation checks must run against this record, never against the
314
+ * defaults they might have come from.
315
+ * @typedef {object} ClientHello
316
+ * @property {Uint8Array} message framed handshake message, ready for the record layer
317
+ * @property {Uint8Array} clientRandom
318
+ * @property {Uint8Array} legacySessionId
319
+ * @property {number[]} offeredCiphers
320
+ * @property {number[]} offeredGroups
321
+ * @property {number[]} offeredSigSchemes
322
+ * @property {Set<number>} offeredExtensions extension types present in the hello
323
+ * @property {string[]} offeredAlpn
324
+ * @property {number} [binderOffset] psk only: where the binder's bytes sit in `message`
325
+ * @property {number} [truncatedLength] psk only: how many leading bytes of `message` the binder
326
+ * transcript covers (RFC 8446 s4.2.11.2 truncation — everything except the binders list)
327
+ */
328
+ /**
329
+ * Build a ClientHello. Returns the framed handshake message plus the metadata the rest of the
330
+ * handshake needs to police the server's answer.
331
+ *
332
+ * @param {ClientHelloOptions} opts
333
+ * @returns {ClientHello}
334
+ */
335
+ /**
336
+ * Extension emission order, by type. This is not cosmetic: JA3 and JA4 hash the extension list in
337
+ * WIRE ORDER, so the order alone is a large part of what a fingerprinter reads.
338
+ *
339
+ * Captured from curl 8.21.0 / OpenSSL 3.6.3, which sends:
340
+ * renegotiation_info, server_name, ec_point_formats, supported_groups, ALPN, encrypt_then_mac,
341
+ * extended_master_secret, post_handshake_auth, signature_algorithms, supported_versions,
342
+ * psk_key_exchange_modes, key_share
343
+ *
344
+ * Two of those this package does not send, and the reason is the same in both cases — an extension
345
+ * is a claim about what we can do. encrypt_then_mac only applies to CBC suites, which are not
346
+ * offered; post_handshake_auth invites a CertificateRequest after the handshake, which is not
347
+ * implemented. status_request goes the other way: curl does not send it, this package does,
348
+ * because a stapled OCSP response is its only revocation signal. It is placed where OpenSSL puts
349
+ * it when it does send one, right after server_name.
350
+ *
351
+ * Anything not named here keeps its natural position at the end, and pre_shared_key is forced last
352
+ * whatever the caller asks for, because RFC 8446 s4.2.11 defines the binder transcript as the hello
353
+ * truncated just before the binders — a range that only exists if nothing follows them.
354
+ */
355
+ /** `extensionOrder: SHUFFLE_EXTENSIONS` reproduces what Chromium does — see grease.js. */
356
+ export const SHUFFLE_EXTENSIONS: "shuffle";
357
+ export const CURL_EXTENSION_ORDER: readonly number[];
311
358
  export { GROUP_PARAMS };
312
359
  /**
313
360
  * An ephemeral key share: the public half as sent in key_share, plus the private key the
@@ -324,6 +371,18 @@ export type KeyShare = {
324
371
  */
325
372
  privateKey: CryptoKey;
326
373
  };
374
+ /**
375
+ * A parsed ServerHello. `isHelloRetryRequest` is decided by the random alone (RFC 8446 s4.1.3);
376
+ * everything else is exactly what the wire carried, judged later by the negotiate* functions.
377
+ */
378
+ export type ServerHello = {
379
+ legacyVersion: number;
380
+ random: Uint8Array;
381
+ legacySessionIdEcho: Uint8Array;
382
+ cipherSuite: number;
383
+ extensions: Map<number, Uint8Array>;
384
+ isHelloRetryRequest: boolean;
385
+ };
327
386
  export type ClientHelloOptions = {
328
387
  /**
329
388
  * SNI, unless it is an IP literal (then no SNI is sent)
@@ -415,16 +474,4 @@ export type ClientHello = {
415
474
  */
416
475
  truncatedLength?: number | undefined;
417
476
  };
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
477
  import { GROUP_PARAMS } from './constants.js';