tunnelfetch 1.0.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.
Files changed (96) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +617 -0
  3. package/README.zh-CN.md +470 -0
  4. package/package.json +74 -0
  5. package/src/client/cookies.js +429 -0
  6. package/src/client/decode.js +346 -0
  7. package/src/client/redirect.js +249 -0
  8. package/src/client.js +704 -0
  9. package/src/errors.js +181 -0
  10. package/src/http1/chunked.js +289 -0
  11. package/src/http1/index.js +10 -0
  12. package/src/http1/request.js +143 -0
  13. package/src/http1/response.js +493 -0
  14. package/src/http2/connection.js +1170 -0
  15. package/src/http2/constants.js +129 -0
  16. package/src/http2/frames.js +291 -0
  17. package/src/http2/hpack.js +420 -0
  18. package/src/http2/huffman.js +203 -0
  19. package/src/http2/index.js +21 -0
  20. package/src/index.js +46 -0
  21. package/src/pool.js +256 -0
  22. package/src/proxy/direct.js +62 -0
  23. package/src/proxy/http-connect.js +206 -0
  24. package/src/proxy/index.js +197 -0
  25. package/src/proxy/socks5.js +344 -0
  26. package/src/tls/aead.js +263 -0
  27. package/src/tls/connect.js +407 -0
  28. package/src/tls/constants.js +334 -0
  29. package/src/tls/extensions.js +376 -0
  30. package/src/tls/handshake-messages.js +901 -0
  31. package/src/tls/handshake.js +568 -0
  32. package/src/tls/handshake12.js +507 -0
  33. package/src/tls/index.js +44 -0
  34. package/src/tls/keyschedule.js +473 -0
  35. package/src/tls/record.js +872 -0
  36. package/src/tls/tickets.js +145 -0
  37. package/src/tls/transcript.js +101 -0
  38. package/src/tls/wire.js +224 -0
  39. package/src/transport.js +296 -0
  40. package/src/trust/der.js +551 -0
  41. package/src/trust/index.js +375 -0
  42. package/src/trust/name.js +235 -0
  43. package/src/trust/ocsp.js +759 -0
  44. package/src/trust/path.js +595 -0
  45. package/src/trust/roots.js +454 -0
  46. package/src/trust/x509.js +902 -0
  47. package/src/util/bytes.js +470 -0
  48. package/src/util/deadline.js +266 -0
  49. package/src/warmup-fixture.js +85 -0
  50. package/src/warmup.js +243 -0
  51. package/types/client/cookies.d.ts +159 -0
  52. package/types/client/decode.d.ts +54 -0
  53. package/types/client/redirect.d.ts +96 -0
  54. package/types/client.d.ts +323 -0
  55. package/types/errors.d.ts +141 -0
  56. package/types/http1/chunked.d.ts +48 -0
  57. package/types/http1/index.d.ts +3 -0
  58. package/types/http1/request.d.ts +44 -0
  59. package/types/http1/response.d.ts +183 -0
  60. package/types/http2/connection.d.ts +282 -0
  61. package/types/http2/constants.d.ts +95 -0
  62. package/types/http2/frames.d.ts +116 -0
  63. package/types/http2/hpack.d.ts +99 -0
  64. package/types/http2/huffman.d.ts +21 -0
  65. package/types/http2/index.d.ts +5 -0
  66. package/types/index.d.ts +17 -0
  67. package/types/pool.d.ts +135 -0
  68. package/types/proxy/direct.d.ts +26 -0
  69. package/types/proxy/http-connect.d.ts +37 -0
  70. package/types/proxy/index.d.ts +62 -0
  71. package/types/proxy/socks5.d.ts +47 -0
  72. package/types/tls/aead.d.ts +67 -0
  73. package/types/tls/connect.d.ts +280 -0
  74. package/types/tls/constants.d.ts +275 -0
  75. package/types/tls/extensions.d.ts +195 -0
  76. package/types/tls/handshake-messages.d.ts +430 -0
  77. package/types/tls/handshake.d.ts +90 -0
  78. package/types/tls/handshake12.d.ts +35 -0
  79. package/types/tls/index.d.ts +9 -0
  80. package/types/tls/keyschedule.d.ts +272 -0
  81. package/types/tls/record.d.ts +361 -0
  82. package/types/tls/tickets.d.ts +66 -0
  83. package/types/tls/transcript.d.ts +52 -0
  84. package/types/tls/wire.d.ts +106 -0
  85. package/types/transport.d.ts +222 -0
  86. package/types/trust/der.d.ts +239 -0
  87. package/types/trust/index.d.ts +194 -0
  88. package/types/trust/name.d.ts +33 -0
  89. package/types/trust/ocsp.d.ts +138 -0
  90. package/types/trust/path.d.ts +139 -0
  91. package/types/trust/roots.d.ts +36 -0
  92. package/types/trust/x509.d.ts +401 -0
  93. package/types/util/bytes.d.ts +183 -0
  94. package/types/util/deadline.d.ts +133 -0
  95. package/types/warmup-fixture.d.ts +11 -0
  96. package/types/warmup.d.ts +45 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Everything that decides whether two requests may share a socket. Mirrors what openConnection
3
+ * consumed to build the connection, because anything that influenced the connection must
4
+ * influence the key.
5
+ * @typedef {object} PoolKeyInput
6
+ * @property {string} scheme the URL protocol, colon included ('http:' | 'https:')
7
+ * @property {string} hostname
8
+ * @property {number} port
9
+ * @property {import('./proxy/index.js').ProxyConfig | null | undefined} proxy
10
+ * @property {import('./trust/index.js').TrustConfig | null | undefined} trust
11
+ * @property {import('./tls/connect.js').TlsOptions | null | undefined} tls
12
+ */
13
+ /**
14
+ * The trust configuration is part of the key. Two requests to the same origin under different
15
+ * verification policies must not share a connection — the peer was validated under one policy and
16
+ * silently reusing it satisfies the other policy without ever having checked it.
17
+ *
18
+ * @param {PoolKeyInput} input
19
+ * @returns {string}
20
+ */
21
+ export function poolKey({ scheme, hostname, port, proxy, trust, tls }: PoolKeyInput): string;
22
+ /**
23
+ * What the pool stores: the connection object openConnection resolves to. The pool itself only
24
+ * ever calls `close?.()`, but naming the real type keeps take() useful to a caller.
25
+ * @typedef {import('./transport.js').Connection} PooledConnection
26
+ */
27
+ /**
28
+ * @typedef {object} PoolOptions
29
+ * @property {number} [maxPerKey] idle connections kept per key, default 6
30
+ * @property {number} [maxTotal] idle connections kept across all keys, default 24
31
+ */
32
+ /**
33
+ * Running counters, never reset. `discarded` includes connections refused at release time;
34
+ * `evicted` counts victims pushed out by a newer release under a full pool.
35
+ * @typedef {object} PoolStats
36
+ * @property {number} hits
37
+ * @property {number} misses
38
+ * @property {number} released
39
+ * @property {number} discarded
40
+ * @property {number} evicted
41
+ */
42
+ export class ConnectionPool {
43
+ /**
44
+ * @param {PoolOptions} [opts]
45
+ */
46
+ constructor({ maxPerKey, maxTotal }?: PoolOptions);
47
+ /** @type {Map<string, Array<{conn: PooledConnection}>>} */
48
+ _idle: Map<string, Array<{
49
+ conn: PooledConnection;
50
+ }>>;
51
+ _total: number;
52
+ _maxPerKey: number;
53
+ _maxTotal: number;
54
+ _closed: boolean;
55
+ /** @type {PoolStats} */
56
+ stats: PoolStats;
57
+ get idleCount(): number;
58
+ /**
59
+ * Take an idle connection for `key`, or null. Most-recently-used first: it is likeliest live.
60
+ * @param {string} key
61
+ * @returns {PooledConnection | null}
62
+ */
63
+ take(key: string): PooledConnection | null;
64
+ /**
65
+ * Offer a connection back. Callers must have proven the body reached its declared end; this
66
+ * method cannot verify that and deliberately does not pretend to — `eligible` is the caller's
67
+ * assertion, and the one place it is computed is the HTTP framing layer.
68
+ * Idle entries carry no age, deliberately. Ageing them out would only narrow the window in
69
+ * which a peer reaps a socket we still believe in, never close it — the peer can hang up at any
70
+ * instant, including the one after the check. What actually makes reuse safe is the recovery in
71
+ * sendAndReceive(): a reused connection that ends without producing one response byte is proof
72
+ * the request was never seen, and it is re-sent on a fresh connection. An age field would look
73
+ * like a second line of defence while being neither necessary nor sufficient.
74
+ *
75
+ * @param {string} key
76
+ * @param {PooledConnection} conn
77
+ * @param {boolean} eligible
78
+ * @returns {boolean} whether the connection was retained
79
+ */
80
+ release(key: string, conn: PooledConnection, eligible: boolean): boolean;
81
+ /**
82
+ * Close and forget one connection that must not be reused.
83
+ * @param {PooledConnection} conn
84
+ * @returns {Promise<void>}
85
+ */
86
+ discard(conn: PooledConnection): Promise<void>;
87
+ /**
88
+ * Close everything. A Client that is done must call this or sockets leak for the isolate.
89
+ * @returns {Promise<void>}
90
+ */
91
+ closeAll(): Promise<void>;
92
+ _assertOpen(): void;
93
+ }
94
+ /**
95
+ * Everything that decides whether two requests may share a socket. Mirrors what openConnection
96
+ * consumed to build the connection, because anything that influenced the connection must
97
+ * influence the key.
98
+ */
99
+ export type PoolKeyInput = {
100
+ /**
101
+ * the URL protocol, colon included ('http:' | 'https:')
102
+ */
103
+ scheme: string;
104
+ hostname: string;
105
+ port: number;
106
+ proxy: import("./proxy/index.js").ProxyConfig | null | undefined;
107
+ trust: import("./trust/index.js").TrustConfig | null | undefined;
108
+ tls: import("./tls/connect.js").TlsOptions | null | undefined;
109
+ };
110
+ /**
111
+ * What the pool stores: the connection object openConnection resolves to. The pool itself only
112
+ * ever calls `close?.()`, but naming the real type keeps take() useful to a caller.
113
+ */
114
+ export type PooledConnection = import("./transport.js").Connection;
115
+ export type PoolOptions = {
116
+ /**
117
+ * idle connections kept per key, default 6
118
+ */
119
+ maxPerKey?: number | undefined;
120
+ /**
121
+ * idle connections kept across all keys, default 24
122
+ */
123
+ maxTotal?: number | undefined;
124
+ };
125
+ /**
126
+ * Running counters, never reset. `discarded` includes connections refused at release time;
127
+ * `evicted` counts victims pushed out by a newer release under a full pool.
128
+ */
129
+ export type PoolStats = {
130
+ hits: number;
131
+ misses: number;
132
+ released: number;
133
+ discarded: number;
134
+ evicted: number;
135
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @typedef {object} DirectOptions
3
+ * @property {{ hostname: string, port: number }} target
4
+ * @property {import('./index.js').ConnectFn} connect injected socket factory
5
+ * @property {AbortSignal} [signal]
6
+ */
7
+ /**
8
+ * Dial the target itself. Resolves with the raw socket duplex; a refused or failed dial throws
9
+ * ProxyError (PROXY_UNREACHABLE) quoting the runtime's own message, which is the best
10
+ * diagnostic a caller will get.
11
+ *
12
+ * @param {DirectOptions} args
13
+ * @returns {Promise<import('./index.js').Duplex>}
14
+ */
15
+ export function openDirect({ target, connect, signal }: DirectOptions): Promise<import("./index.js").Duplex>;
16
+ export type DirectOptions = {
17
+ target: {
18
+ hostname: string;
19
+ port: number;
20
+ };
21
+ /**
22
+ * injected socket factory
23
+ */
24
+ connect: import("./index.js").ConnectFn;
25
+ signal?: AbortSignal | undefined;
26
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Establish a CONNECT tunnel through an http/https proxy. Resolves with the tunnel duplex;
3
+ * every refusal (non-2xx, 407 with or without credentials, malformed reply) throws a
4
+ * ProxyError naming what the proxy answered.
5
+ *
6
+ * @param {HttpConnectOptions} args
7
+ * @returns {Promise<ProxyTunnel>}
8
+ */
9
+ export function openHttpConnect({ proxy, target, connect, signal, limits }: HttpConnectOptions): Promise<ProxyTunnel>;
10
+ /**
11
+ * The tunnel a proxy module hands back: the byte duplex plus the underlying socket, kept so a
12
+ * caller that must tear down the transport can reach past the wrapping streams.
13
+ */
14
+ export type ProxyTunnel = import("./index.js").Duplex & {
15
+ socket: import("./index.js").Duplex;
16
+ };
17
+ export type HttpConnectOptions = {
18
+ /**
19
+ * protocol 'http' or 'https'
20
+ */
21
+ proxy: import("./index.js").ProxyConfig;
22
+ target: {
23
+ hostname: string;
24
+ port: number;
25
+ };
26
+ /**
27
+ * injected socket factory
28
+ */
29
+ connect: import("./index.js").ConnectFn;
30
+ signal?: AbortSignal | undefined;
31
+ /**
32
+ * CONNECT reply head cap, default 32768
33
+ */
34
+ limits?: {
35
+ maxProxyReplyBytes?: number;
36
+ } | undefined;
37
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Normalise a proxy spec. Accepts a URL string (`http://user:pass@host:8080`,
3
+ * `socks5://host:1080`) or an object.
4
+ *
5
+ * `socks5h` is accepted as an alias of `socks5` because that is the spelling curl popularised for
6
+ * "resolve names at the proxy" — which is the only mode this package implements, since the
7
+ * runtime gives us no resolver and remote resolution is also what avoids leaking the target to
8
+ * the local DNS path.
9
+ *
10
+ * @param {string | ProxyConfig | null | undefined} spec
11
+ * @returns {ProxyConfig | null}
12
+ */
13
+ export function parseProxy(spec: string | ProxyConfig | null | undefined): ProxyConfig | null;
14
+ /**
15
+ * Open a byte tunnel to `target`, through `proxy` if given.
16
+ *
17
+ * @param {object} args
18
+ * @param {ProxyConfig | string | null} [args.proxy] null/absent means a direct connection
19
+ * @param {{hostname: string, port: number}} args.target
20
+ * @param {ConnectFn} args.connect socket factory, injected
21
+ * @param {AbortSignal} [args.signal]
22
+ * @param {object} [args.limits]
23
+ * @returns {Promise<Duplex & { proxied: boolean }>}
24
+ */
25
+ export function openTunnel({ proxy, target, connect, signal, limits }: {
26
+ proxy?: string | ProxyConfig | null | undefined;
27
+ target: {
28
+ hostname: string;
29
+ port: number;
30
+ };
31
+ connect: ConnectFn;
32
+ signal?: AbortSignal | undefined;
33
+ limits?: object | undefined;
34
+ }): Promise<Duplex & {
35
+ proxied: boolean;
36
+ }>;
37
+ /** Close a duplex without caring whether it was already gone. */
38
+ export function closeQuietly(duplex: any): Promise<void>;
39
+ export type Duplex = {
40
+ readable: ReadableStream<Uint8Array>;
41
+ writable: WritableStream<Uint8Array>;
42
+ opened?: Promise<unknown>;
43
+ close?: () => Promise<void>;
44
+ };
45
+ export type ConnectFn = (addr: {
46
+ hostname: string;
47
+ port: number;
48
+ }, opts?: {
49
+ secureTransport?: "off" | "on" | "starttls";
50
+ allowHalfOpen?: boolean;
51
+ }) => Duplex;
52
+ export type ProxyConfig = {
53
+ protocol: "http" | "https" | "socks5" | "socks5h";
54
+ hostname: string;
55
+ port: number;
56
+ username?: string;
57
+ password?: string;
58
+ };
59
+ import { openDirect } from './direct.js';
60
+ import { openHttpConnect } from './http-connect.js';
61
+ import { openSocks5 } from './socks5.js';
62
+ export { openDirect, openHttpConnect, openSocks5 };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @typedef {object} Socks5Options
3
+ * @property {import('./index.js').ProxyConfig} proxy credentials trigger RFC 1929 user/pass auth
4
+ * @property {{ hostname: string, port: number }} target
5
+ * @property {import('./index.js').ConnectFn} connect injected socket factory
6
+ * @property {AbortSignal} [signal]
7
+ */
8
+ /**
9
+ * Establish a SOCKS5 tunnel. Resolves with the tunnel duplex; every refusal (no acceptable
10
+ * auth method, rejected credentials, non-zero reply code, unframeable reply) throws a
11
+ * ProxyError naming the exact wire value the proxy sent.
12
+ *
13
+ * @param {Socks5Options} args
14
+ * @returns {Promise<import('./http-connect.js').ProxyTunnel>}
15
+ */
16
+ export function openSocks5({ proxy, target, connect, signal }: Socks5Options): Promise<import("./http-connect.js").ProxyTunnel>;
17
+ /**
18
+ * Encode DST.ADDR + DST.PORT. Prefers the domain form so the proxy resolves.
19
+ * @param {{ hostname: string, port: number }} target
20
+ * @returns {Uint8Array}
21
+ */
22
+ export function encodeAddress(target: {
23
+ hostname: string;
24
+ port: number;
25
+ }): Uint8Array;
26
+ /**
27
+ * Minimal IPv6 text parser: `::` compression and a trailing embedded IPv4 are both real.
28
+ * Throws ConfigError on anything that does not expand to exactly 8 groups.
29
+ * @param {string} text
30
+ * @returns {Uint8Array} the 16 address bytes
31
+ */
32
+ export function parseIpv6(text: string): Uint8Array;
33
+ export type Socks5Options = {
34
+ /**
35
+ * credentials trigger RFC 1929 user/pass auth
36
+ */
37
+ proxy: import("./index.js").ProxyConfig;
38
+ target: {
39
+ hostname: string;
40
+ port: number;
41
+ };
42
+ /**
43
+ * injected socket factory
44
+ */
45
+ connect: import("./index.js").ConnectFn;
46
+ signal?: AbortSignal | undefined;
47
+ };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * TLS 1.3 per-record nonce: the 64-bit sequence number left-padded to the IV length, XORed
3
+ * with the static IV. Exported so the tests can pin the construction independently of a full
4
+ * encrypt round trip.
5
+ * @param {Uint8Array} iv
6
+ * @param {number | bigint} seq
7
+ * @returns {Uint8Array}
8
+ */
9
+ export function buildNonce(iv: Uint8Array, seq: number | bigint): Uint8Array;
10
+ /**
11
+ * Record protection for one direction under one key. `encrypt` returns the encrypted record
12
+ * body ready for framing; `decrypt` either returns authenticated plaintext (with the inner
13
+ * content type under 1.3, the header type under 1.2) or throws TLS_RECORD — never garbage.
14
+ * @typedef {object} Aead
15
+ * @property {number} version
16
+ * @property {(seq: number | bigint, type: number, plaintext: Uint8Array,
17
+ * opts?: { padding?: number }) => Promise<Uint8Array>} encrypt
18
+ * @property {(seq: number | bigint, body: Uint8Array, header: Uint8Array)
19
+ * => Promise<{ type: number, plaintext: Uint8Array }>} decrypt
20
+ */
21
+ /**
22
+ * @typedef {object} AeadOptions
23
+ * @property {number} [version] `TLS13` (default) or `TLS12`; picks nonce and AAD construction
24
+ * @property {number} cipher cipher suite id, must have CIPHER_PARAMS
25
+ * @property {Uint8Array} key
26
+ * @property {Uint8Array} iv the 12-byte static IV for TLS 1.3, the 4-byte implicit salt for
27
+ * TLS 1.2
28
+ */
29
+ /**
30
+ * Create record protection for one direction under one key. A new key (handshake -> application,
31
+ * KeyUpdate) means a new instance; sequence numbers restart with it.
32
+ *
33
+ * @param {AeadOptions} opts
34
+ * @returns {Promise<Aead>}
35
+ */
36
+ export function createAead({ version, cipher, key, iv }: AeadOptions): Promise<Aead>;
37
+ /**
38
+ * Record protection for one direction under one key. `encrypt` returns the encrypted record
39
+ * body ready for framing; `decrypt` either returns authenticated plaintext (with the inner
40
+ * content type under 1.3, the header type under 1.2) or throws TLS_RECORD — never garbage.
41
+ */
42
+ export type Aead = {
43
+ version: number;
44
+ encrypt: (seq: number | bigint, type: number, plaintext: Uint8Array, opts?: {
45
+ padding?: number;
46
+ }) => Promise<Uint8Array>;
47
+ decrypt: (seq: number | bigint, body: Uint8Array, header: Uint8Array) => Promise<{
48
+ type: number;
49
+ plaintext: Uint8Array;
50
+ }>;
51
+ };
52
+ export type AeadOptions = {
53
+ /**
54
+ * `TLS13` (default) or `TLS12`; picks nonce and AAD construction
55
+ */
56
+ version?: number | undefined;
57
+ /**
58
+ * cipher suite id, must have CIPHER_PARAMS
59
+ */
60
+ cipher: number;
61
+ key: Uint8Array;
62
+ /**
63
+ * the 12-byte static IV for TLS 1.3, the 4-byte implicit salt for
64
+ * TLS 1.2
65
+ */
66
+ iv: Uint8Array;
67
+ };
@@ -0,0 +1,280 @@
1
+ /**
2
+ * A byte duplex: what every layer in this package consumes and produces.
3
+ * @typedef {{ readable: ReadableStream<Uint8Array>,
4
+ * writable: WritableStream<Uint8Array> }} ByteDuplex
5
+ */
6
+ /**
7
+ * Handshake knobs. Every one of these narrows what is offered; none can widen it beyond what
8
+ * `constants.js` permits, so no option here can talk the client into a suite it refuses.
9
+ *
10
+ * @typedef {object} TlsOptions
11
+ * @property {number[]} [versions] versions to offer, from `TLS13` / `TLS12`. Default both.
12
+ * @property {string[]} [alpn] ALPN protocols to offer. Default `['http/1.1']`.
13
+ * @property {number[]} [groups] supported_groups, in preference order.
14
+ * @property {number[]} [offerGroups] groups to send an actual key_share for. Default the first
15
+ * supported group; a HelloRetryRequest recovers any other choice at the cost of a round trip.
16
+ * @property {number[]} [ciphers] cipher suites to offer, in preference order.
17
+ * @property {Uint8Array} [clientRandom] fixed ClientHello.random, for reproducible handshakes.
18
+ * @property {Uint8Array} [legacySessionId] fixed legacy_session_id, likewise.
19
+ * @property {boolean} [compatibilityCcs] send the middlebox-compatibility ChangeCipherSpec.
20
+ * Default true.
21
+ * @property {number} [maxHandshakeMessage] per-message cap; certificate chains dominate sizing.
22
+ * @property {number} [maxKeyUpdates] received KeyUpdates tolerated before it is called a flood.
23
+ * @property {number} [maxTranscriptBytes] cap on buffered handshake transcript.
24
+ * @property {ResumptionOffer} [psk] offer this resumption PSK (TLS 1.3 only; requires 1.3 in
25
+ * the offered versions). The server may decline, in which case the full handshake continues
26
+ * on this same connection — there is no reconnect at any layer.
27
+ * @property {(ticket: CapturedTicket) => void} [onSessionTicket] receive each NewSessionTicket
28
+ * this connection yields, already reduced to a usable PSK per RFC 8446 s7.1. Without this the
29
+ * tickets are read and discarded, exactly as before.
30
+ */
31
+ /**
32
+ * A resumption PSK ready to offer, as produced by the ticket store from a CapturedTicket.
33
+ * `obfuscatedTicketAge` is a closure, not a number, because the age must be current at the
34
+ * moment each hello is BUILT — a HelloRetryRequest builds a second hello later — and because
35
+ * clock policy belongs to the store, not to this layer (which otherwise never reads a clock).
36
+ * `peer` rides along opaquely: it is whatever the original session's verifyPeer resolved with,
37
+ * and a resumed session (which has no Certificate message to verify) reports it as its own —
38
+ * sound only because the ticket store keys tickets by the full trust configuration.
39
+ * @typedef {object} ResumptionOffer
40
+ * @property {Uint8Array} identity the ticket
41
+ * @property {Uint8Array} psk
42
+ * @property {import('./keyschedule.js').ScheduleHash} hash the hash the PSK was minted under
43
+ * @property {() => number} obfuscatedTicketAge uint32 per RFC 8446 s4.2.11.1
44
+ * @property {object} [peer]
45
+ */
46
+ /**
47
+ * What a NewSessionTicket becomes by the time a caller sees it: the wire fields that govern
48
+ * offering (lifetime, age_add) plus the derived PSK and everything needed to check a future
49
+ * selection against it. `maxEarlyDataSize` is recorded for honesty but never acted on: 0-RTT
50
+ * is deliberately not implemented (see the driver's note).
51
+ * @typedef {object} CapturedTicket
52
+ * @property {Uint8Array} identity
53
+ * @property {Uint8Array} psk
54
+ * @property {import('./keyschedule.js').ScheduleHash} hash
55
+ * @property {number} cipherSuite
56
+ * @property {number} lifetimeSec
57
+ * @property {number} ageAdd
58
+ * @property {number | null} maxEarlyDataSize
59
+ * @property {string | null} alpnProtocol
60
+ * @property {object} peer
61
+ */
62
+ /**
63
+ * Injectable nondeterminism. Supplying these makes a handshake byte-for-byte reproducible, which
64
+ * is what allows a recorded session to be replayed in an offline test.
65
+ * @typedef {object} TlsDeps
66
+ * @property {(n: number) => Uint8Array} [randomBytes]
67
+ * @property {(algorithm: object, group: number) => Promise<CryptoKeyPair>} [generateKeyPair]
68
+ */
69
+ /**
70
+ * What a completed handshake reports about itself.
71
+ * @typedef {object} TlsSessionInfo
72
+ * @property {number} version negotiated version, `0x0304` or `0x0303`
73
+ * @property {number} cipherSuite negotiated suite
74
+ * @property {number} group negotiated key-exchange group
75
+ * @property {string | null} alpnProtocol
76
+ * @property {string} hostname the identity the certificate was required to prove
77
+ * @property {boolean} [extendedMasterSecret] TLS 1.2 only: whether RFC 7627 was in effect
78
+ * @property {boolean} [resumed] TLS 1.3 only: the server accepted the offered resumption PSK,
79
+ * so no certificate crossed the wire on THIS connection; the identity is the one validated
80
+ * by the original handshake the ticket came from
81
+ */
82
+ /**
83
+ * A live TLS session: a plaintext duplex plus what was negotiated to get it.
84
+ * @typedef {object} TlsSession
85
+ * @property {ReadableStream<Uint8Array>} readable
86
+ * @property {WritableStream<Uint8Array>} writable
87
+ * @property {import('./record.js').RecordLayer} record
88
+ * @property {object} peer whatever `verifyPeer` resolved with: the validated leaf
89
+ * @property {TlsSessionInfo} info
90
+ * @property {() => Promise<void>} close
91
+ */
92
+ /**
93
+ * Run a TLS handshake over a byte duplex, negotiating the version, and return the plaintext
94
+ * duplex above it. The default offer is [TLS 1.3, TLS 1.2]; `options.versions` narrows it.
95
+ *
96
+ * @param {object} args
97
+ * @param {ByteDuplex} args.transport
98
+ * @param {string} args.hostname the identity the certificate must prove, and the SNI sent
99
+ * @param {import('./handshake.js').VerifyPeer} args.verifyPeer
100
+ * Must throw to reject. Resolves with the validated leaf; its SPKI is the only key either
101
+ * driver will accept a handshake signature from. Receives the peer's stapled OCSP response,
102
+ * when there is one, as its third argument.
103
+ * @param {TlsOptions} [args.options]
104
+ * @param {TlsDeps} [args.deps]
105
+ * @returns {Promise<TlsSession>}
106
+ */
107
+ export function connectTls({ transport, hostname, verifyPeer, options, deps }: {
108
+ transport: ByteDuplex;
109
+ hostname: string;
110
+ verifyPeer: import("./handshake.js").VerifyPeer;
111
+ options?: TlsOptions | undefined;
112
+ deps?: TlsDeps | undefined;
113
+ }): Promise<TlsSession>;
114
+ /**
115
+ * A byte duplex: what every layer in this package consumes and produces.
116
+ */
117
+ export type ByteDuplex = {
118
+ readable: ReadableStream<Uint8Array>;
119
+ writable: WritableStream<Uint8Array>;
120
+ };
121
+ /**
122
+ * Handshake knobs. Every one of these narrows what is offered; none can widen it beyond what
123
+ * `constants.js` permits, so no option here can talk the client into a suite it refuses.
124
+ */
125
+ export type TlsOptions = {
126
+ /**
127
+ * versions to offer, from `TLS13` / `TLS12`. Default both.
128
+ */
129
+ versions?: number[] | undefined;
130
+ /**
131
+ * ALPN protocols to offer. Default `['http/1.1']`.
132
+ */
133
+ alpn?: string[] | undefined;
134
+ /**
135
+ * supported_groups, in preference order.
136
+ */
137
+ groups?: number[] | undefined;
138
+ /**
139
+ * groups to send an actual key_share for. Default the first
140
+ * supported group; a HelloRetryRequest recovers any other choice at the cost of a round trip.
141
+ */
142
+ offerGroups?: number[] | undefined;
143
+ /**
144
+ * cipher suites to offer, in preference order.
145
+ */
146
+ ciphers?: number[] | undefined;
147
+ /**
148
+ * fixed ClientHello.random, for reproducible handshakes.
149
+ */
150
+ clientRandom?: Uint8Array<ArrayBufferLike> | undefined;
151
+ /**
152
+ * fixed legacy_session_id, likewise.
153
+ */
154
+ legacySessionId?: Uint8Array<ArrayBufferLike> | undefined;
155
+ /**
156
+ * send the middlebox-compatibility ChangeCipherSpec.
157
+ * Default true.
158
+ */
159
+ compatibilityCcs?: boolean | undefined;
160
+ /**
161
+ * per-message cap; certificate chains dominate sizing.
162
+ */
163
+ maxHandshakeMessage?: number | undefined;
164
+ /**
165
+ * received KeyUpdates tolerated before it is called a flood.
166
+ */
167
+ maxKeyUpdates?: number | undefined;
168
+ /**
169
+ * cap on buffered handshake transcript.
170
+ */
171
+ maxTranscriptBytes?: number | undefined;
172
+ /**
173
+ * offer this resumption PSK (TLS 1.3 only; requires 1.3 in
174
+ * the offered versions). The server may decline, in which case the full handshake continues
175
+ * on this same connection — there is no reconnect at any layer.
176
+ */
177
+ psk?: ResumptionOffer | undefined;
178
+ /**
179
+ * receive each NewSessionTicket
180
+ * this connection yields, already reduced to a usable PSK per RFC 8446 s7.1. Without this the
181
+ * tickets are read and discarded, exactly as before.
182
+ */
183
+ onSessionTicket?: ((ticket: CapturedTicket) => void) | undefined;
184
+ };
185
+ /**
186
+ * A resumption PSK ready to offer, as produced by the ticket store from a CapturedTicket.
187
+ * `obfuscatedTicketAge` is a closure, not a number, because the age must be current at the
188
+ * moment each hello is BUILT — a HelloRetryRequest builds a second hello later — and because
189
+ * clock policy belongs to the store, not to this layer (which otherwise never reads a clock).
190
+ * `peer` rides along opaquely: it is whatever the original session's verifyPeer resolved with,
191
+ * and a resumed session (which has no Certificate message to verify) reports it as its own —
192
+ * sound only because the ticket store keys tickets by the full trust configuration.
193
+ */
194
+ export type ResumptionOffer = {
195
+ /**
196
+ * the ticket
197
+ */
198
+ identity: Uint8Array;
199
+ psk: Uint8Array;
200
+ /**
201
+ * the hash the PSK was minted under
202
+ */
203
+ hash: import("./keyschedule.js").ScheduleHash;
204
+ /**
205
+ * uint32 per RFC 8446 s4.2.11.1
206
+ */
207
+ obfuscatedTicketAge: () => number;
208
+ peer?: object | undefined;
209
+ };
210
+ /**
211
+ * What a NewSessionTicket becomes by the time a caller sees it: the wire fields that govern
212
+ * offering (lifetime, age_add) plus the derived PSK and everything needed to check a future
213
+ * selection against it. `maxEarlyDataSize` is recorded for honesty but never acted on: 0-RTT
214
+ * is deliberately not implemented (see the driver's note).
215
+ */
216
+ export type CapturedTicket = {
217
+ identity: Uint8Array;
218
+ psk: Uint8Array;
219
+ hash: import("./keyschedule.js").ScheduleHash;
220
+ cipherSuite: number;
221
+ lifetimeSec: number;
222
+ ageAdd: number;
223
+ maxEarlyDataSize: number | null;
224
+ alpnProtocol: string | null;
225
+ peer: object;
226
+ };
227
+ /**
228
+ * Injectable nondeterminism. Supplying these makes a handshake byte-for-byte reproducible, which
229
+ * is what allows a recorded session to be replayed in an offline test.
230
+ */
231
+ export type TlsDeps = {
232
+ randomBytes?: ((n: number) => Uint8Array) | undefined;
233
+ generateKeyPair?: ((algorithm: object, group: number) => Promise<CryptoKeyPair>) | undefined;
234
+ };
235
+ /**
236
+ * What a completed handshake reports about itself.
237
+ */
238
+ export type TlsSessionInfo = {
239
+ /**
240
+ * negotiated version, `0x0304` or `0x0303`
241
+ */
242
+ version: number;
243
+ /**
244
+ * negotiated suite
245
+ */
246
+ cipherSuite: number;
247
+ /**
248
+ * negotiated key-exchange group
249
+ */
250
+ group: number;
251
+ alpnProtocol: string | null;
252
+ /**
253
+ * the identity the certificate was required to prove
254
+ */
255
+ hostname: string;
256
+ /**
257
+ * TLS 1.2 only: whether RFC 7627 was in effect
258
+ */
259
+ extendedMasterSecret?: boolean | undefined;
260
+ /**
261
+ * TLS 1.3 only: the server accepted the offered resumption PSK,
262
+ * so no certificate crossed the wire on THIS connection; the identity is the one validated
263
+ * by the original handshake the ticket came from
264
+ */
265
+ resumed?: boolean | undefined;
266
+ };
267
+ /**
268
+ * A live TLS session: a plaintext duplex plus what was negotiated to get it.
269
+ */
270
+ export type TlsSession = {
271
+ readable: ReadableStream<Uint8Array>;
272
+ writable: WritableStream<Uint8Array>;
273
+ record: import("./record.js").RecordLayer;
274
+ /**
275
+ * whatever `verifyPeer` resolved with: the validated leaf
276
+ */
277
+ peer: object;
278
+ info: TlsSessionInfo;
279
+ close: () => Promise<void>;
280
+ };