quico 0.1.3 → 0.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,655 @@
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 { Duplex, Readable, Writable } from 'node:stream';
16
+ import { EventEmitter } from 'node:events';
17
+ import dns from 'node:dns';
18
+ import { H3Connection } from './h3.js';
19
+ import { createQuicClientSocket } from './quic_socket.js';
20
+ import { readVarInt, concatUint8Arrays, wt_stream_header, wt_datagram_payload } from './utils.js';
21
+
22
+
23
+ // ============================================================
24
+ // WebTransportBidiStream — Duplex + WHATWG .readable/.writable
25
+ //
26
+ // Node.js style:
27
+ // stream.write(data);
28
+ // stream.on('data', chunk => { ... });
29
+ // stream.pipe(dest);
30
+ //
31
+ // WHATWG style:
32
+ // const writer = stream.writable.getWriter();
33
+ // const reader = stream.readable.getReader();
34
+ // ============================================================
35
+
36
+ class WebTransportBidiStream extends Duplex {
37
+
38
+ constructor(options) {
39
+ super({ highWaterMark: (options && options.highWaterMark) || 64 * 1024 });
40
+
41
+ this._streamId = options.streamId;
42
+ this._quic = options.quic;
43
+ this._sessionId = options.sessionId;
44
+ this._prefixSent = options.prefixSent || false;
45
+
46
+ // Lazy WHATWG wrappers
47
+ this._whatwgReadable = null;
48
+ this._whatwgWritable = null;
49
+ }
50
+
51
+ // ---- WHATWG getters ----
52
+
53
+ get readable() {
54
+ if (!this._whatwgReadable) {
55
+ var self = this;
56
+ this._whatwgReadable = new ReadableStream({
57
+ start(controller) {
58
+ self.on('data', function (chunk) {
59
+ controller.enqueue(chunk);
60
+ });
61
+ self.on('end', function () {
62
+ controller.close();
63
+ });
64
+ self.on('error', function (err) {
65
+ controller.error(err);
66
+ });
67
+ }
68
+ });
69
+ }
70
+ return this._whatwgReadable;
71
+ }
72
+
73
+ get writable() {
74
+ if (!this._whatwgWritable) {
75
+ var self = this;
76
+ this._whatwgWritable = new WritableStream({
77
+ write(chunk) {
78
+ return new Promise(function (resolve, reject) {
79
+ self.write(chunk, function (err) { err ? reject(err) : resolve(); });
80
+ });
81
+ },
82
+ close() {
83
+ return new Promise(function (resolve) { self.end(resolve); });
84
+ }
85
+ });
86
+ }
87
+ return this._whatwgWritable;
88
+ }
89
+
90
+ // ---- Node.js Duplex implementation ----
91
+
92
+ _read(_size) { /* data pushed externally via _pushData */ }
93
+
94
+ _write(chunk, encoding, callback) {
95
+ if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
96
+ if (Buffer.isBuffer(chunk)) {
97
+ chunk = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
98
+ }
99
+
100
+ // First write on this stream: prepend VarInt(0x41) + VarInt(session_id)
101
+ if (!this._prefixSent) {
102
+ this._prefixSent = true;
103
+ this._quic.sendStream(this._streamId, wt_stream_header(true, this._sessionId, chunk), false);
104
+ } else {
105
+ this._quic.sendStream(this._streamId, chunk, false);
106
+ }
107
+ callback();
108
+ }
109
+
110
+ _final(callback) {
111
+ // Send FIN (with type + session_id prefix if nothing was written yet)
112
+ if (!this._prefixSent) {
113
+ this._prefixSent = true;
114
+ this._quic.sendStream(this._streamId, wt_stream_header(true, this._sessionId), true);
115
+ } else {
116
+ this._quic.sendStream(this._streamId, new Uint8Array(0), true);
117
+ }
118
+ callback();
119
+ }
120
+
121
+ /**
122
+ * Called by WebTransport when data arrives for this stream.
123
+ */
124
+ _pushData(chunk) {
125
+ if (Buffer.isBuffer(chunk)) this.push(chunk);
126
+ else if (chunk instanceof Uint8Array) this.push(Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength));
127
+ else this.push(chunk);
128
+ }
129
+
130
+ _endData() {
131
+ this.push(null);
132
+ }
133
+ }
134
+
135
+
136
+ // ============================================================
137
+ // WebTransportUniStream — Writable (outgoing) or Readable (incoming)
138
+ // ============================================================
139
+
140
+ class WebTransportUniWriteStream extends Writable {
141
+
142
+ constructor(options) {
143
+ super({ highWaterMark: (options && options.highWaterMark) || 64 * 1024 });
144
+ this._streamId = options.streamId;
145
+ this._quic = options.quic;
146
+ this._sessionId = options.sessionId;
147
+ this._headerSent = false;
148
+ }
149
+
150
+ _write(chunk, encoding, callback) {
151
+ if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
152
+ if (Buffer.isBuffer(chunk)) {
153
+ chunk = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
154
+ }
155
+
156
+ if (!this._headerSent) {
157
+ this._headerSent = true;
158
+ // Uni stream: VarInt(0x54) (WEBTRANSPORT_STREAM) + VarInt(session_id) + data
159
+ this._quic.sendStream(this._streamId, wt_stream_header(false, this._sessionId, chunk), false);
160
+ } else {
161
+ this._quic.sendStream(this._streamId, chunk, false);
162
+ }
163
+ callback();
164
+ }
165
+
166
+ _final(callback) {
167
+ if (!this._headerSent) {
168
+ this._headerSent = true;
169
+ this._quic.sendStream(this._streamId, wt_stream_header(false, this._sessionId), true);
170
+ } else {
171
+ this._quic.sendStream(this._streamId, new Uint8Array(0), true);
172
+ }
173
+ callback();
174
+ }
175
+
176
+ // WHATWG getter
177
+ get writable() {
178
+ if (!this._whatwgWritable) {
179
+ var self = this;
180
+ this._whatwgWritable = new WritableStream({
181
+ write(chunk) {
182
+ return new Promise(function (resolve, reject) {
183
+ self.write(chunk, function (err) { err ? reject(err) : resolve(); });
184
+ });
185
+ },
186
+ close() {
187
+ return new Promise(function (resolve) { self.end(resolve); });
188
+ }
189
+ });
190
+ }
191
+ return this._whatwgWritable;
192
+ }
193
+ }
194
+
195
+
196
+ class WebTransportUniReadStream extends Readable {
197
+
198
+ constructor(options) {
199
+ super({ highWaterMark: (options && options.highWaterMark) || 64 * 1024 });
200
+ this._streamId = options.streamId;
201
+ }
202
+
203
+ _read(_size) { /* data pushed externally */ }
204
+
205
+ _pushData(chunk) {
206
+ if (Buffer.isBuffer(chunk)) this.push(chunk);
207
+ else if (chunk instanceof Uint8Array) this.push(Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength));
208
+ else this.push(chunk);
209
+ }
210
+
211
+ _endData() { this.push(null); }
212
+
213
+ // WHATWG getter
214
+ get readable() {
215
+ if (!this._whatwgReadable) {
216
+ var self = this;
217
+ this._whatwgReadable = new ReadableStream({
218
+ start(controller) {
219
+ self.on('data', function (chunk) { controller.enqueue(chunk); });
220
+ self.on('end', function () { controller.close(); });
221
+ self.on('error', function (err) { controller.error(err); });
222
+ }
223
+ });
224
+ }
225
+ return this._whatwgReadable;
226
+ }
227
+ }
228
+
229
+
230
+ // ============================================================
231
+ // WebTransport — Client
232
+ //
233
+ // Usage:
234
+ // var wt = new WebTransport('https://server:4433/path');
235
+ // await wt.ready;
236
+ //
237
+ // // Bidi stream (Duplex)
238
+ // var stream = await wt.createBidirectionalStream();
239
+ // stream.write('hello');
240
+ // stream.on('data', chunk => console.log(chunk));
241
+ //
242
+ // // Also works as WHATWG:
243
+ // var writer = stream.writable.getWriter();
244
+ // var reader = stream.readable.getReader();
245
+ //
246
+ // // Uni stream (outgoing only)
247
+ // var uni = await wt.createUnidirectionalStream();
248
+ // uni.write('one-way data');
249
+ // uni.end();
250
+ //
251
+ // // Datagrams
252
+ // wt.sendDatagram(Buffer.from('ping'));
253
+ // wt.on('datagram', data => console.log('pong:', data));
254
+ //
255
+ // // Incoming streams
256
+ // wt.on('stream', stream => { ... }); // bidi from server
257
+ // wt.on('unidirectionalStream', stream => { ... }); // uni from server
258
+ //
259
+ // wt.close();
260
+ // ============================================================
261
+
262
+ class WebTransport extends EventEmitter {
263
+
264
+ constructor(url, options) {
265
+ super();
266
+
267
+ // Parse URL
268
+ var parsed = new URL(url);
269
+ this._hostname = parsed.hostname;
270
+ this._port = parseInt(parsed.port) || 443;
271
+ this._path = parsed.pathname + (parsed.search || '');
272
+ this._options = options || {};
273
+
274
+ // State
275
+ this._state = 'connecting'; // connecting → connected → closed
276
+ this._sessionId = null; // stream ID of the CONNECT request
277
+ this._quic = null;
278
+ this._h3 = null;
279
+ this._udpSocket = null;
280
+
281
+ // Stream tracking
282
+ this._streams = {}; // streamId → WebTransportBidiStream | ReadStream
283
+ this._claimedStreams = new Set(); // stream IDs owned by WT (not H3)
284
+
285
+ // Client-initiated stream ID counters
286
+ // Bidi: 0 used for CONNECT, so WT bidi starts at 4, 8, 12...
287
+ // Uni: 2, 6, 10 used for H3 control/qpack, so WT uni starts at 14, 18, 22...
288
+ this._nextBidiStreamId = 4;
289
+ this._nextUniStreamId = 14;
290
+
291
+ // Incoming stream prefix buffers (for parsing session_id)
292
+ this._incomingBuffers = {};
293
+
294
+ // Promises (browser-compatible API)
295
+ var self = this;
296
+ this.ready = new Promise(function (resolve, reject) {
297
+ self._readyResolve = resolve;
298
+ self._readyReject = reject;
299
+ });
300
+
301
+ this.closed = new Promise(function (resolve) {
302
+ self._closedResolve = resolve;
303
+ });
304
+
305
+ // Start connection
306
+ this._connect();
307
+ }
308
+
309
+
310
+ // ---- Connection setup ----
311
+
312
+ _connect() {
313
+ var host = this._hostname;
314
+ var self = this;
315
+
316
+ if (/^[\d.]+$/.test(host) || host.indexOf(':') >= 0 || host === 'localhost') {
317
+ var ip = (host === 'localhost') ? '127.0.0.1' : host;
318
+ this._startQuic(ip);
319
+ } else {
320
+ dns.lookup(host, function (err, address) {
321
+ if (err) { self._readyReject(err); self.emit('error', err); return; }
322
+ self._startQuic(address);
323
+ });
324
+ }
325
+ }
326
+
327
+ _startQuic(remoteIp) {
328
+ var self = this;
329
+ var remotePort = this._port;
330
+
331
+ this._udpSocket = createQuicClientSocket({
332
+ remoteIp: remoteIp,
333
+ remotePort: remotePort,
334
+ hostname: this._hostname,
335
+
336
+ onError: function (err) {
337
+ if (self._state === 'connecting') self._readyReject(err);
338
+ self.emit('error', err);
339
+ },
340
+
341
+ onConnect: function (q, s) {
342
+ self._quic = q;
343
+ self._udpSocket = s;
344
+ self._h3 = new H3Connection({ quicConnection: self._quic, isServer: false, enableWebTransport: true });
345
+ self._setupH3Handlers();
346
+ self._sendConnect();
347
+ },
348
+
349
+ onClose: function () {
350
+ self._state = 'closed';
351
+ self._closedResolve();
352
+ self.emit('close');
353
+ // The helper closes the UDP socket.
354
+ }
355
+ });
356
+ }
357
+
358
+
359
+ // ---- H3 + QUIC event handlers ----
360
+
361
+ _setupH3Handlers() {
362
+ var self = this;
363
+
364
+ // CONNECT response headers
365
+ this._h3.on('http_headers', function (streamId, headers) {
366
+ if (streamId === self._sessionId) {
367
+ var status = parseInt(headers[':status']) || 0;
368
+ if (status === 200) {
369
+ self._state = 'connected';
370
+ self._readyResolve();
371
+ self.emit('ready');
372
+ } else {
373
+ var err = new Error('WebTransport CONNECT failed: HTTP ' + status);
374
+ self._readyReject(err);
375
+ self.emit('error', err);
376
+ }
377
+ }
378
+ });
379
+
380
+ // Raw QUIC stream data — intercept WT streams before H3 processes them
381
+ // We register on QUIC directly. H3 will also receive these events,
382
+ // but we call h3.claimStream() for WT streams so H3 skips them.
383
+ this._quic.on('stream', function (streamId, data, fin) {
384
+ // Skip streams owned by H3 (CONNECT stream, control streams)
385
+ if (streamId === self._sessionId) return; // H3 handles CONNECT response
386
+ if (!self._isWebTransportStream(streamId)) return;
387
+
388
+ self._handleStreamData(streamId, data, fin);
389
+ });
390
+
391
+ // Datagrams — QUIC DATAGRAM payload = VarInt(quarter_stream_id) + app data (RFC 9297)
392
+ this._quic.on('datagram', function (rawData) {
393
+ if (!rawData || rawData.byteLength === 0) return;
394
+
395
+ // Always parse VarInt quarter_stream_id from payload
396
+ var result = readVarInt(rawData, 0);
397
+ if (!result) return;
398
+ var qsid = result.value;
399
+ var payload = rawData.slice(result.byteLength);
400
+
401
+ var expectedQsid = Math.floor(self._sessionId / 4);
402
+ if (self._sessionId !== null && qsid === expectedQsid) {
403
+ self.emit('datagram', Buffer.from(payload));
404
+ }
405
+ });
406
+ }
407
+
408
+ _isWebTransportStream(streamId) {
409
+ // Already claimed
410
+ if (this._claimedStreams.has(streamId)) return true;
411
+
412
+ // Server-initiated bidi: 1, 5, 9, 13... (could be WT)
413
+ // Server-initiated uni: 3, 7, 11, 15... (11+ could be WT, 3/7/11 are H3)
414
+ // Client-initiated bidi: 4+ are WT (0 is CONNECT)
415
+ // Client-initiated uni: 14+ are WT (2/6/10 are H3)
416
+ var type = streamId & 0x3;
417
+
418
+ if (type === 0x0) {
419
+ // Client bidi — stream 0 is CONNECT, 4+ are WT
420
+ return streamId >= 4;
421
+ } else if (type === 0x1) {
422
+ // Server bidi — all are potentially WT
423
+ return true;
424
+ } else if (type === 0x2) {
425
+ // Client uni — 2/6/10 are H3, 14+ are WT
426
+ return streamId >= 14;
427
+ } else {
428
+ // Server uni — 3/7/11 are H3, 15+ are potentially WT
429
+ return streamId >= 15;
430
+ }
431
+ }
432
+
433
+ _handleStreamData(streamId, data, fin) {
434
+ // Already have a stream object?
435
+ if (streamId in this._streams) {
436
+ var stream = this._streams[streamId];
437
+ if (data.byteLength > 0) stream._pushData(data);
438
+ if (fin) { stream._endData(); delete this._streams[streamId]; }
439
+ return;
440
+ }
441
+
442
+ // New incoming stream — need to parse session_id prefix
443
+ if (!(streamId in this._incomingBuffers)) {
444
+ this._incomingBuffers[streamId] = { chunks: [], totalLen: 0 };
445
+ }
446
+
447
+ var buf = this._incomingBuffers[streamId];
448
+ buf.chunks.push(data);
449
+ buf.totalLen += data.byteLength;
450
+
451
+ // Try to parse prefix
452
+ var combined = buf.totalLen === data.byteLength ? data : concatUint8Arrays(buf.chunks);
453
+ var isBidi = (streamId & 0x2) === 0;
454
+ var offset = 0;
455
+
456
+ // Parse stream type as VarInt (0x41=bidi WT, 0x54=uni WT)
457
+ var typeResult = readVarInt(combined, 0);
458
+ if (!typeResult) { if (fin) delete this._incomingBuffers[streamId]; return; }
459
+ var streamType = typeResult.value;
460
+ offset = typeResult.byteLength;
461
+
462
+ if (!isBidi) {
463
+ if (streamType !== 0x54) {
464
+ // Not a WT uni stream — let H3 handle it
465
+ delete this._incomingBuffers[streamId];
466
+ return;
467
+ }
468
+ } else {
469
+ if (streamType !== 0x41) {
470
+ delete this._incomingBuffers[streamId];
471
+ return;
472
+ }
473
+ }
474
+
475
+ // Parse VarInt session_id
476
+ if (offset >= combined.byteLength) { if (fin) delete this._incomingBuffers[streamId]; return; }
477
+ var result = readVarInt(combined, offset);
478
+ if (!result) { if (fin) delete this._incomingBuffers[streamId]; return; }
479
+
480
+ var sessionId = result.value;
481
+ var afterPrefix = offset + result.byteLength;
482
+
483
+ // Verify it matches our session
484
+ if (sessionId !== this._sessionId) {
485
+ delete this._incomingBuffers[streamId];
486
+ return;
487
+ }
488
+
489
+ // Claim stream — tell H3 to skip it
490
+ this._claimedStreams.add(streamId);
491
+ if (this._h3 && this._h3.claimStream) this._h3.claimStream(streamId);
492
+ delete this._incomingBuffers[streamId];
493
+
494
+ // Create stream object
495
+ var remaining = combined.byteLength > afterPrefix
496
+ ? combined.slice(afterPrefix)
497
+ : new Uint8Array(0);
498
+
499
+ if (isBidi) {
500
+ var stream = new WebTransportBidiStream({
501
+ streamId: streamId,
502
+ quic: this._quic,
503
+ sessionId: this._sessionId,
504
+ prefixSent: false // server-initiated, prefix not needed for writes back
505
+ });
506
+ this._streams[streamId] = stream;
507
+ if (remaining.byteLength > 0) stream._pushData(remaining);
508
+ if (fin) { stream._endData(); delete this._streams[streamId]; }
509
+ this.emit('stream', stream);
510
+
511
+ } else {
512
+ var stream = new WebTransportUniReadStream({ streamId: streamId });
513
+ this._streams[streamId] = stream;
514
+ if (remaining.byteLength > 0) stream._pushData(remaining);
515
+ if (fin) { stream._endData(); delete this._streams[streamId]; }
516
+ this.emit('unidirectionalStream', stream);
517
+ }
518
+ }
519
+
520
+ // ---- Send CONNECT ----
521
+
522
+ _sendConnect() {
523
+ // Send H3 control streams (SETTINGS)
524
+ this._h3.sendControlStreams();
525
+
526
+ // CONNECT uses stream 0 (first client bidi)
527
+ this._sessionId = 0;
528
+
529
+ // Send Extended CONNECT — no FIN (session stays open)
530
+ var headers = {
531
+ ':method': 'CONNECT',
532
+ ':protocol': 'webtransport',
533
+ ':scheme': 'https',
534
+ ':authority': this._hostname + (this._port !== 443 ? ':' + this._port : ''),
535
+ ':path': this._path
536
+ };
537
+
538
+ this._h3.sendHeaders(this._sessionId, headers, false);
539
+ }
540
+
541
+
542
+ // ============================================================
543
+ // Public API
544
+ // ============================================================
545
+
546
+ /**
547
+ * Create a bidirectional stream.
548
+ * Returns a Duplex stream (Node.js) with .readable/.writable (WHATWG).
549
+ */
550
+ createBidirectionalStream() {
551
+ var self = this;
552
+ return new Promise(function (resolve, reject) {
553
+ if (self._state !== 'connected') {
554
+ return reject(new Error('WebTransport is not connected'));
555
+ }
556
+
557
+ var streamId = self._nextBidiStreamId;
558
+ self._nextBidiStreamId += 4;
559
+
560
+ // Claim this stream so H3 doesn't process it
561
+ self._claimedStreams.add(streamId);
562
+ if (self._h3 && self._h3.claimStream) self._h3.claimStream(streamId);
563
+
564
+ var stream = new WebTransportBidiStream({
565
+ streamId: streamId,
566
+ quic: self._quic,
567
+ sessionId: self._sessionId,
568
+ prefixSent: false
569
+ });
570
+
571
+ self._streams[streamId] = stream;
572
+
573
+ // Listen for incoming data on this bidi stream
574
+ // (already handled by _handleStreamData via QUIC stream event)
575
+
576
+ resolve(stream);
577
+ });
578
+ }
579
+
580
+ /**
581
+ * Create a unidirectional stream (outgoing only).
582
+ * Returns a Writable stream (Node.js) with .writable (WHATWG).
583
+ */
584
+ createUnidirectionalStream() {
585
+ var self = this;
586
+ return new Promise(function (resolve, reject) {
587
+ if (self._state !== 'connected') {
588
+ return reject(new Error('WebTransport is not connected'));
589
+ }
590
+
591
+ var streamId = self._nextUniStreamId;
592
+ self._nextUniStreamId += 4;
593
+
594
+ // Claim this stream
595
+ self._claimedStreams.add(streamId);
596
+ if (self._h3 && self._h3.claimStream) self._h3.claimStream(streamId);
597
+
598
+ var stream = new WebTransportUniWriteStream({
599
+ streamId: streamId,
600
+ quic: self._quic,
601
+ sessionId: self._sessionId
602
+ });
603
+
604
+ resolve(stream);
605
+ });
606
+ }
607
+
608
+ /**
609
+ * Send an unreliable datagram.
610
+ */
611
+ sendDatagram(data) {
612
+ if (this._state !== 'connected') throw new Error('WebTransport is not connected');
613
+ if (typeof data === 'string') data = Buffer.from(data);
614
+ if (Buffer.isBuffer(data)) data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
615
+
616
+ // Build datagram: VarInt(quarter_stream_id) + payload (RFC 9297)
617
+ var frame = wt_datagram_payload(this._sessionId, data);
618
+
619
+ // Send via QUIC DATAGRAM frame
620
+ if (this._quic && this._quic.sendDatagram) {
621
+ this._quic.sendDatagram(frame);
622
+ }
623
+ }
624
+
625
+ /**
626
+ * Close the WebTransport session.
627
+ */
628
+ close(info) {
629
+ if (this._state === 'closed') return;
630
+ this._state = 'closed';
631
+
632
+ // Send CLOSE_WEBTRANSPORT_SESSION on CONNECT stream (optional)
633
+ // Then close QUIC connection
634
+ if (this._quic) {
635
+ this._quic.close(0, (info && info.reason) || 'WebTransport closed');
636
+ }
637
+ }
638
+
639
+ /**
640
+ * Current state.
641
+ */
642
+ get state() {
643
+ return this._state;
644
+ }
645
+
646
+ /**
647
+ * URL this session connected to.
648
+ */
649
+ get url() {
650
+ return 'https://' + this._hostname + ':' + this._port + this._path;
651
+ }
652
+ }
653
+
654
+
655
+ export { WebTransport, WebTransportBidiStream, WebTransportUniWriteStream, WebTransportUniReadStream };
package/CHANGELOG.md DELETED
@@ -1,31 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [0.1.0] - 2025-08-07
9
- ### Added
10
- - Basic QUIC server with 1-RTT support
11
- - TLS 1.3 handshake (fully JS)
12
- - HTTP/3 request/response over control streams
13
- - Static & dynamic QPACK decoding
14
- - Basic `createServer()` API
15
- - Quick start example in README
16
-
17
- ### In progress
18
- - WebTransport streams (uni & bidi)
19
- - 0-RTT support
20
- - Integration with Node.js `https`-style API
21
- - Connection teardown, keep-alive, congestion control
22
-
23
- ### Notes
24
- - This is a pre-release version under heavy development.
25
- - API may change without notice.
26
-
27
-
28
- ## [0.1.1] - 2025-09-30
29
- - Extracted the TLS implementation into a separate library: **lemon-tls**, for easier maintenance and reuse.
30
- - Refactored the project’s file structure for clearer organization and improved modularity.
31
- - Migrated the codebase from CommonJS (`require`) to **ESM** (`import/export`) for modern compatibility.