quico 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/streams.js ADDED
@@ -0,0 +1,471 @@
1
+ /*
2
+ * quico: HTTP/3 and QUIC implementation for Node.js
3
+ * Copyright 2025 colocohen
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at:
8
+ *
9
+ * https://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * This file is part of the open-source project hosted at:
12
+ * https://github.com/colocohen/quico
13
+ */
14
+
15
+ import { Readable, Writable, Duplex } from 'node:stream';
16
+
17
+
18
+ // ============================================================
19
+ // IncomingMessage — extends Readable
20
+ //
21
+ // Wraps incoming HTTP data:
22
+ // - On server: the request body (req)
23
+ // - On client: the response body (res)
24
+ //
25
+ // Data is pushed in from the H3 layer via _pushData / _endData.
26
+ // Consumers use pipe(), for-await-of, on('data'), etc.
27
+ // ============================================================
28
+
29
+ class IncomingMessage extends Readable {
30
+
31
+ constructor(options) {
32
+ super({ highWaterMark: (options && options.highWaterMark) || 64 * 1024 });
33
+
34
+ options = options || {};
35
+
36
+ this.httpVersion = options.httpVersion || '3.0';
37
+ this.httpVersionMajor = parseInt(this.httpVersion) || 3;
38
+ this.httpVersionMinor = parseInt((this.httpVersion.split('.')[1]) || '0') || 0;
39
+
40
+ this.complete = false;
41
+ this.aborted = false;
42
+
43
+ this.headers = options.headers || {};
44
+ this.rawHeaders = [];
45
+ this.trailers = {};
46
+ this.rawTrailers = [];
47
+
48
+ // Client-side (response)
49
+ this.statusCode = options.statusCode || null;
50
+ this.statusMessage = options.statusMessage || '';
51
+
52
+ // Server-side (request)
53
+ this.method = options.method || null;
54
+ this.url = options.url || null;
55
+
56
+ // QUIC has no TCP socket — stub so libraries don't crash
57
+ this.socket = options.socket || _makeSocketStub();
58
+ this.connection = this.socket;
59
+
60
+ // H3 stream id (internal)
61
+ this._streamId = options.streamId != null ? options.streamId : null;
62
+ this._timeoutTimer = null;
63
+
64
+ // Build rawHeaders
65
+ for (var name in this.headers) {
66
+ this.rawHeaders.push(name, String(this.headers[name]));
67
+ }
68
+ }
69
+
70
+ // Readable requires _read — data is pushed externally, so no-op.
71
+ _read(_size) { /* no-op */ }
72
+
73
+ /**
74
+ * Called by H3 layer when a body chunk arrives.
75
+ */
76
+ _pushData(chunk) {
77
+ if (this.complete) return;
78
+ if (typeof chunk === 'string') chunk = Buffer.from(chunk);
79
+ if (chunk instanceof Uint8Array && !Buffer.isBuffer(chunk)) {
80
+ chunk = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
81
+ }
82
+ this.push(chunk);
83
+ }
84
+
85
+ /**
86
+ * Called by H3 layer when the stream ends (FIN received).
87
+ */
88
+ _endData() {
89
+ if (this.complete) return;
90
+ this.complete = true;
91
+ this.push(null);
92
+ }
93
+
94
+ /**
95
+ * Abort the stream (error / timeout / reset).
96
+ */
97
+ _abort(err) {
98
+ if (this.complete) return;
99
+ this.aborted = true;
100
+ this.complete = true;
101
+ if (err) this.destroy(err);
102
+ else this.push(null);
103
+ }
104
+
105
+ setTimeout(msecs, callback) {
106
+ if (callback) this.once('timeout', callback);
107
+ if (this._timeoutTimer) clearTimeout(this._timeoutTimer);
108
+ if (msecs > 0) {
109
+ this._timeoutTimer = setTimeout(() => this.emit('timeout'), msecs);
110
+ }
111
+ return this;
112
+ }
113
+ }
114
+
115
+
116
+ // ============================================================
117
+ // ServerResponse — extends Writable
118
+ //
119
+ // Wraps outgoing HTTP response on the server side.
120
+ // write() / pipe(res) / end() → h3.sendHeaders + h3.sendBody
121
+ // ============================================================
122
+
123
+ class ServerResponse extends Writable {
124
+
125
+ constructor(options) {
126
+ super({ highWaterMark: (options && options.highWaterMark) || 64 * 1024 });
127
+
128
+ options = options || {};
129
+
130
+ this.statusCode = 200;
131
+ this.statusMessage = 'OK';
132
+ this.headersSent = false;
133
+ this.finished = false;
134
+ this._headers = {};
135
+ this._headerNames = {}; // lowercase → original-case
136
+
137
+ // H3 references (set by server when creating the response)
138
+ this._streamId = options.streamId != null ? options.streamId : null;
139
+ this._h3 = options.h3 || null;
140
+
141
+ // Paired request (Express compat: res.req)
142
+ this.req = options.req || null;
143
+
144
+ this.socket = options.socket || _makeSocketStub();
145
+ this.connection = this.socket;
146
+
147
+ this.on('finish', () => {
148
+ this.finished = true;
149
+ this.emit('close');
150
+ });
151
+
152
+ // Express does res.__proto__ = app.response (http.ServerResponse.prototype)
153
+ // which kills our class methods. Bind them to the instance so they survive.
154
+ this.setHeader = this.setHeader.bind(this);
155
+ this.getHeader = this.getHeader.bind(this);
156
+ this.getHeaders = this.getHeaders.bind(this);
157
+ this.getHeaderNames = this.getHeaderNames.bind(this);
158
+ this.hasHeader = this.hasHeader.bind(this);
159
+ this.removeHeader = this.removeHeader.bind(this);
160
+ this.writeHead = this.writeHead.bind(this);
161
+ this.flushHeaders = this.flushHeaders.bind(this);
162
+ this._flushHeaders = this._flushHeaders.bind(this);
163
+ this.write = this.write.bind(this);
164
+ this.end = this.end.bind(this);
165
+ }
166
+
167
+ // ---- Header management (mirrors node:http) ----
168
+
169
+ setHeader(name, value) {
170
+ if (this.headersSent) throw new Error('Cannot set headers after they are sent to the client');
171
+ var lower = name.toLowerCase();
172
+ this._headers[lower] = value;
173
+ this._headerNames[lower] = name;
174
+ return this;
175
+ }
176
+
177
+ getHeader(name) {
178
+ return this._headers[name.toLowerCase()];
179
+ }
180
+
181
+ getHeaders() {
182
+ var out = {};
183
+ for (var k in this._headers) out[k] = this._headers[k];
184
+ return out;
185
+ }
186
+
187
+ getHeaderNames() {
188
+ return Object.keys(this._headers);
189
+ }
190
+
191
+ hasHeader(name) {
192
+ return name.toLowerCase() in this._headers;
193
+ }
194
+
195
+ removeHeader(name) {
196
+ if (this.headersSent) throw new Error('Cannot remove headers after they are sent to the client');
197
+ var lower = name.toLowerCase();
198
+ delete this._headers[lower];
199
+ delete this._headerNames[lower];
200
+ return this;
201
+ }
202
+
203
+ // ---- writeHead ----
204
+
205
+ writeHead(statusCode, statusMessage, headers) {
206
+ if (this.headersSent) return this;
207
+
208
+ if (typeof statusMessage === 'object' && !headers) {
209
+ headers = statusMessage;
210
+ statusMessage = undefined;
211
+ }
212
+
213
+ this.statusCode = statusCode;
214
+ if (statusMessage !== undefined) this.statusMessage = String(statusMessage);
215
+
216
+ if (typeof headers === 'object' && headers !== null) {
217
+ for (var name in headers) this.setHeader(name, headers[name]);
218
+ }
219
+
220
+ this._flushHeaders();
221
+ return this;
222
+ }
223
+
224
+ flushHeaders() {
225
+ if (!this.headersSent) this._flushHeaders();
226
+ }
227
+
228
+ _flushHeaders() {
229
+ if (this.headersSent || !this._h3) return;
230
+
231
+ var h3h = { ':status': String(this.statusCode) };
232
+ for (var lower in this._headers) h3h[lower] = String(this._headers[lower]);
233
+
234
+ this._h3.sendHeaders(this._streamId, h3h);
235
+ this.headersSent = true;
236
+ }
237
+
238
+ // ---- Writable implementation ----
239
+
240
+ _write(chunk, encoding, callback) {
241
+ if (!this.headersSent) this._flushHeaders();
242
+
243
+ if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
244
+ if (Buffer.isBuffer(chunk)) {
245
+ chunk = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
246
+ }
247
+
248
+ if (this._h3) this._h3.sendBody(this._streamId, chunk, false);
249
+ callback();
250
+ }
251
+
252
+ _final(callback) {
253
+ if (!this.headersSent) this._flushHeaders();
254
+ if (this._h3) this._h3.sendBody(this._streamId, null, true);
255
+ callback();
256
+ }
257
+
258
+ // ---- node:http compat overrides ----
259
+
260
+ write(chunk, encoding, callback) {
261
+ if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
262
+ return super.write(chunk, encoding, callback);
263
+ }
264
+
265
+ end(data, encoding, callback) {
266
+ if (typeof data === 'function') { callback = data; data = undefined; encoding = undefined; }
267
+ if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
268
+ if (data != null) this.write(data, encoding);
269
+ return super.end(callback);
270
+ }
271
+
272
+ writeContinue() { /* HTTP/3 doesn't use 100-continue — stub */ }
273
+
274
+ setTimeout(msecs, callback) {
275
+ if (callback) this.once('timeout', callback);
276
+ return this;
277
+ }
278
+ }
279
+
280
+
281
+ // ============================================================
282
+ // ClientRequest — extends Writable
283
+ //
284
+ // Wraps outgoing HTTP request on the client side.
285
+ // write() / pipe(req) buffers body data.
286
+ // _final() signals body-complete so the transport can send.
287
+ //
288
+ // The 'response' event delivers an IncomingMessage.
289
+ // ============================================================
290
+
291
+ class ClientRequest extends Writable {
292
+
293
+ constructor(options) {
294
+ super({ highWaterMark: (options && options.highWaterMark) || 64 * 1024 });
295
+
296
+ options = options || {};
297
+
298
+ this.method = (options.method || 'GET').toUpperCase();
299
+ this.path = options.path || '/';
300
+ this.host = options.hostname || options.host || 'localhost';
301
+ this.port = options.port || 443;
302
+
303
+ this.headersSent = false;
304
+ this.finished = false;
305
+ this.aborted = false;
306
+ this.reusedSocket = false;
307
+
308
+ this._headers = {};
309
+ this._headerNames = {};
310
+
311
+ // Copy initial headers
312
+ if (options.headers) {
313
+ for (var name in options.headers) this.setHeader(name, options.headers[name]);
314
+ }
315
+
316
+ // Body buffer — accumulated via write/pipe, sent when _final fires
317
+ this._bodyChunks = [];
318
+
319
+ // H3 internals — set by unified client after protocol selection
320
+ this._h3 = null;
321
+ this._streamId = null;
322
+ this._quic = null;
323
+
324
+ // The callback from request(opts, cb)
325
+ this._responseCallback = options._responseCallback || null;
326
+
327
+ // Protocol that was actually used (filled after connect)
328
+ this.protocol = null; // 'h3', 'h2', 'https'
329
+
330
+ this.socket = options.socket || _makeSocketStub();
331
+ this.connection = this.socket;
332
+
333
+ this._timeoutTimer = null;
334
+ this._timeoutMs = 0;
335
+ }
336
+
337
+ // ---- Header management ----
338
+
339
+ setHeader(name, value) {
340
+ var lower = name.toLowerCase();
341
+ this._headers[lower] = value;
342
+ this._headerNames[lower] = name;
343
+ return this;
344
+ }
345
+
346
+ getHeader(name) {
347
+ return this._headers[name.toLowerCase()];
348
+ }
349
+
350
+ getHeaders() {
351
+ var out = {};
352
+ for (var k in this._headers) out[k] = this._headers[k];
353
+ return out;
354
+ }
355
+
356
+ removeHeader(name) {
357
+ var lower = name.toLowerCase();
358
+ delete this._headers[lower];
359
+ delete this._headerNames[lower];
360
+ return this;
361
+ }
362
+
363
+ hasHeader(name) {
364
+ return name.toLowerCase() in this._headers;
365
+ }
366
+
367
+ setNoDelay() { return this; }
368
+ setSocketKeepAlive() { return this; }
369
+
370
+ // ---- Writable implementation ----
371
+
372
+ _write(chunk, encoding, callback) {
373
+ if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
374
+ if (Buffer.isBuffer(chunk)) {
375
+ chunk = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
376
+ }
377
+ this._bodyChunks.push(chunk);
378
+ callback();
379
+ }
380
+
381
+ _final(callback) {
382
+ this.finished = true;
383
+ this.emit('_bodyReady');
384
+ callback();
385
+ }
386
+
387
+ // ---- Convenience overrides ----
388
+
389
+ write(chunk, encoding, callback) {
390
+ if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
391
+ return super.write(chunk, encoding, callback);
392
+ }
393
+
394
+ end(data, encoding, callback) {
395
+ if (typeof data === 'function') { callback = data; data = undefined; encoding = undefined; }
396
+ if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
397
+ if (data != null) this.write(data, encoding);
398
+ return super.end(callback);
399
+ }
400
+
401
+ abort() {
402
+ this.aborted = true;
403
+ this.destroy(new Error('Request aborted'));
404
+ }
405
+
406
+ setTimeout(msecs, callback) {
407
+ if (callback) this.once('timeout', callback);
408
+ this._timeoutMs = msecs;
409
+ if (this._timeoutTimer) clearTimeout(this._timeoutTimer);
410
+ if (msecs > 0) {
411
+ this._timeoutTimer = setTimeout(() => this.emit('timeout'), msecs);
412
+ }
413
+ return this;
414
+ }
415
+
416
+ /**
417
+ * Collect all buffered body chunks into one Uint8Array.
418
+ * Returns null if nothing was written (GET / HEAD).
419
+ */
420
+ _getBody() {
421
+ if (this._bodyChunks.length === 0) return null;
422
+ if (this._bodyChunks.length === 1) return this._bodyChunks[0];
423
+ var total = 0;
424
+ for (var i = 0; i < this._bodyChunks.length; i++) total += this._bodyChunks[i].byteLength;
425
+ var merged = new Uint8Array(total);
426
+ var off = 0;
427
+ for (var i = 0; i < this._bodyChunks.length; i++) {
428
+ merged.set(this._bodyChunks[i], off);
429
+ off += this._bodyChunks[i].byteLength;
430
+ }
431
+ return merged;
432
+ }
433
+ }
434
+
435
+
436
+ // ============================================================
437
+ // Socket stub — for libraries that check req.socket / res.socket
438
+ // Must be a real Stream so Node's eos() / Express don't crash.
439
+ // ============================================================
440
+
441
+ function _makeSocketStub() {
442
+ var sock = new Duplex({
443
+ read(_size) { /* no-op — no data from QUIC socket */ },
444
+ write(_chunk, _encoding, callback) { callback(); }
445
+ });
446
+
447
+ // TLS properties
448
+ sock.encrypted = true;
449
+ sock.authorized = true;
450
+ sock.remoteAddress = null;
451
+ sock.remotePort = null;
452
+ sock.localAddress = null;
453
+ sock.localPort = null;
454
+ sock.alpnProtocol = 'h3';
455
+
456
+ sock.setNoDelay = function () { return sock; };
457
+ sock.setKeepAlive = function () { return sock; };
458
+ sock.setTimeout = function () { return sock; };
459
+ sock.ref = function () { return sock; };
460
+ sock.unref = function () { return sock; };
461
+
462
+ // TLS methods (Express / Helmet / etc.)
463
+ sock.getPeerCertificate = function () { return {}; };
464
+ sock.getProtocol = function () { return 'TLSv1.3'; };
465
+ sock.getCipher = function () { return { name: 'TLS_AES_128_GCM_SHA256', version: 'TLSv1.3' }; };
466
+
467
+ return sock;
468
+ }
469
+
470
+
471
+ export { IncomingMessage, ServerResponse, ClientRequest, _makeSocketStub };
@@ -0,0 +1,211 @@
1
+ /*
2
+ * quico: HTTP/3 and QUIC implementation for Node.js
3
+ * Copyright 2025 colocohen
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at:
8
+ *
9
+ * https://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * This file is part of the open-source project hosted at:
12
+ * https://github.com/colocohen/quico
13
+ */
14
+
15
+ import { TLSSession, createSecureContext as _lemonCreateSecureContext } from 'lemon-tls';
16
+ import nodeTls from 'node:tls';
17
+ import { Emitter } from './utils.js';
18
+ import { build_transport_params } from './transport.js';
19
+ import {
20
+ TLS_CIPHER_SUITES
21
+ } from './crypto.js';
22
+
23
+
24
+ function defaultTransportParams() {
25
+ return {
26
+ max_udp_payload_size: 65527,
27
+ max_idle_timeout: 30000,
28
+ initial_max_data: 1048576,
29
+ initial_max_stream_data_bidi_local: 262144,
30
+ initial_max_stream_data_bidi_remote: 262144,
31
+ initial_max_stream_data_uni: 131072,
32
+ initial_max_streams_bidi: 100,
33
+ initial_max_streams_uni: 3,
34
+ ack_delay_exponent: 3,
35
+ max_ack_delay: 25,
36
+ disable_active_migration: true,
37
+ active_connection_id_limit: 4,
38
+ max_datagram_frame_size: 65527
39
+ };
40
+ }
41
+
42
+
43
+ function TLSBridge(options) {
44
+ options = options || {};
45
+
46
+ var ev = Emitter();
47
+ var session = null;
48
+ var handshakeSecretsEmitted = false;
49
+ var appSecretsEmitted = false;
50
+ var helloHandled = false;
51
+ var isServer = !!options.isServer;
52
+ var originalDcid = options.originalDcid || new Uint8Array(0);
53
+ var localCid = options.localCid || new Uint8Array(0); // client's own SCID
54
+ var hostname = options.hostname || null;
55
+
56
+ function feedMessage(data) {
57
+ if (!session) {
58
+ var sessionOpts = { isServer: isServer, SNICallback: options.SNICallback };
59
+ if (!isServer) {
60
+ sessionOpts.sessionId = new Uint8Array(0); // QUIC: no compatibility mode
61
+ if (hostname) sessionOpts.servername = hostname;
62
+ }
63
+ session = new TLSSession(sessionOpts);
64
+ setupSessionEvents();
65
+
66
+ // Client: set ALPN + transport params NOW, before setTimeout builds ClientHello
67
+ if (!isServer) {
68
+ var tp = defaultTransportParams();
69
+ tp.initial_source_connection_id = localCid;
70
+ var quicParams = build_transport_params(tp);
71
+
72
+ session.set_context({
73
+ local_supported_versions: [0x0304],
74
+ local_supported_alpns: ['h3'],
75
+ local_supported_groups: [29, 23, 24],
76
+ local_supported_cipher_suites: [0x1301, 0x1302],
77
+ local_extensions: [{ type: 0x39, data: quicParams }],
78
+ local_supported_signature_algorithms: [0x0401, 0x0501, 0x0601],
79
+ });
80
+ helloHandled = true; // ClientHello will be generated from this set_context
81
+ }
82
+ }
83
+ session.message(data);
84
+ }
85
+
86
+ function setupSessionEvents() {
87
+
88
+ session.on('hello', function () {
89
+ if (helloHandled) return; // prevent double-fire
90
+ helloHandled = true;
91
+ console.log('[tls] hello — configuring TLS');
92
+
93
+ // Transport params — shared defaults + role-specific fields
94
+ var tp = defaultTransportParams();
95
+
96
+ if (isServer) {
97
+ // Server must echo original DCID and provide SCID
98
+ tp.original_destination_connection_id = originalDcid;
99
+ tp.initial_source_connection_id = originalDcid;
100
+ tp.stateless_reset_token = new Uint8Array(16).fill(0xab); // TODO: random per connection
101
+ } else {
102
+ // Client: initial_source_connection_id = our SCID (not DCID!)
103
+ tp.initial_source_connection_id = localCid;
104
+ }
105
+
106
+ var quicParams = build_transport_params(tp);
107
+
108
+ var tlsContext = {
109
+ local_supported_versions: [0x0304],
110
+ local_supported_alpns: ['h3'],
111
+ local_supported_groups: [29, 23, 24],
112
+ local_supported_cipher_suites: [0x1301, 0x1302, 0xc02f, 0xc030, 0xcca8],
113
+ local_extensions: [
114
+ { type: 0x39, data: quicParams }
115
+ ],
116
+ local_supported_signature_algorithms: [0x0401, 0x0501, 0x0601],
117
+ };
118
+
119
+ session.set_context(tlsContext);
120
+ });
121
+
122
+ session.on('message', function (epoch, seq, type, data) {
123
+ var quicEpoch;
124
+ if (epoch === 0) quicEpoch = 'initial';
125
+ else if (epoch === 1) quicEpoch = 'handshake';
126
+ else quicEpoch = 'app';
127
+
128
+ console.log('[tls] outgoing: epoch=' + quicEpoch + ' type=' + type + ' len=' + data.length);
129
+ ev.emit('send', quicEpoch, data);
130
+ });
131
+
132
+ // Native events from LemonTLS — no polling needed
133
+ session.on('handshakeSecrets', function (localSecret, remoteSecret) {
134
+ if (handshakeSecretsEmitted) return;
135
+ handshakeSecretsEmitted = true;
136
+ var cipher = session.getCipher();
137
+ var hashName = TLS_CIPHER_SUITES[cipher] ? TLS_CIPHER_SUITES[cipher].hash : 'sha256';
138
+ console.log('[tls] handshake secrets ready');
139
+ ev.emit('handshakeSecrets', { local: localSecret, remote: remoteSecret, cipher: cipher, hash: hashName });
140
+ });
141
+
142
+ session.on('appSecrets', function (localSecret, remoteSecret) {
143
+ if (appSecretsEmitted) return;
144
+ appSecretsEmitted = true;
145
+ var cipher = session.getCipher();
146
+ var hashName = TLS_CIPHER_SUITES[cipher] ? TLS_CIPHER_SUITES[cipher].hash : 'sha256';
147
+ console.log('[tls] app secrets ready');
148
+ ev.emit('appSecrets', { local: localSecret, remote: remoteSecret, cipher: cipher, hash: hashName });
149
+ });
150
+
151
+ session.on('secureConnect', function () {
152
+ console.log('[tls] secureConnect');
153
+ ev.emit('secureConnect');
154
+ });
155
+
156
+ session.on('keyUpdate', function (info) {
157
+ ev.emit('keyUpdate', info.direction, info.secret);
158
+ });
159
+ }
160
+
161
+ return {
162
+ on: function (name, fn) { ev.on(name, fn); },
163
+ off: function (name, fn) { ev.off(name, fn); },
164
+ feedMessage: feedMessage,
165
+ requestKeyUpdate: function () { if (session) session.requestKeyUpdate(true); },
166
+ getHandshakeSecrets: function () {
167
+ if (!session) return null;
168
+ var hs = session.getHandshakeSecrets();
169
+ if (!hs || !hs.localSecret) return null;
170
+ var cipher = session.getCipher();
171
+ var hashName = TLS_CIPHER_SUITES[cipher] ? TLS_CIPHER_SUITES[cipher].hash : 'sha256';
172
+ return { local: hs.localSecret, remote: hs.remoteSecret, cipher: cipher, hash: hashName };
173
+ },
174
+ getTrafficSecrets: function () {
175
+ if (!session) return null;
176
+ var secrets = session.getTrafficSecrets();
177
+ if (!secrets || !secrets.localAppSecret) return null;
178
+ var cipher = session.getCipher();
179
+ var hashName = TLS_CIPHER_SUITES[cipher] ? TLS_CIPHER_SUITES[cipher].hash : 'sha256';
180
+ return { local: secrets.localAppSecret, remote: secrets.remoteAppSecret, cipher: cipher, hash: hashName };
181
+ },
182
+ getCipher: function () { return session ? session.getCipher() : null; },
183
+ close: function () { if (session && session.close) session.close(); }
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Drop-in replacement for tls.createSecureContext.
189
+ * Builds both a lemon-tls context (for H3/QUIC) and a node:tls
190
+ * context (for HTTP/2 + HTTP/1.1 over TCP) from the same credentials.
191
+ */
192
+ function createSecureContext(options) {
193
+ var ctx = _lemonCreateSecureContext(options);
194
+ if (ctx && options) {
195
+ ctx._rawKey = options.key || null;
196
+ ctx._rawCert = options.cert || null;
197
+ ctx._rawCa = options.ca || null;
198
+ try {
199
+ var nodeOpts = {};
200
+ if (options.key) nodeOpts.key = options.key;
201
+ if (options.cert) nodeOpts.cert = options.cert;
202
+ if (options.ca) nodeOpts.ca = options.ca;
203
+ ctx._nodeContext = nodeTls.createSecureContext(nodeOpts);
204
+ } catch (e) {
205
+ ctx._nodeContext = null;
206
+ }
207
+ }
208
+ return ctx;
209
+ }
210
+
211
+ export { TLSBridge, createSecureContext };