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.
package/index.d.ts ADDED
@@ -0,0 +1,158 @@
1
+ /*
2
+ * quico — TypeScript declarations
3
+ * Pure JavaScript QUIC, HTTP/3, QPACK & WebTransport for Node.js
4
+ */
5
+
6
+ /// <reference types="node" />
7
+
8
+ import { IncomingMessage, ServerResponse } from 'node:http';
9
+ import { Duplex, Readable, Writable } from 'node:stream';
10
+ import { EventEmitter } from 'node:events';
11
+
12
+ // ============================================================
13
+ // Server
14
+ // ============================================================
15
+
16
+ type RequestHandler = (req: QuicoIncomingMessage, res: QuicoServerResponse) => void;
17
+
18
+ interface ServerOptions {
19
+ key?: string | Buffer;
20
+ cert?: string | Buffer;
21
+ ca?: string | Buffer;
22
+ SNICallback?: (servername: string, cb: (err: Error | null, ctx: any) => void) => void;
23
+ maxConnections?: number;
24
+ }
25
+
26
+ interface QuicoServer extends EventEmitter {
27
+ listen(port: number, callback?: () => void): this;
28
+ listen(port: number, host: string, callback?: () => void): this;
29
+ close(callback?: () => void): void;
30
+ on(event: 'request', listener: RequestHandler): this;
31
+ on(event: 'listening', listener: () => void): this;
32
+ on(event: 'error', listener: (err: Error) => void): this;
33
+ on(event: string, listener: (...args: any[]) => void): this;
34
+ }
35
+
36
+ interface QuicoIncomingMessage extends IncomingMessage {
37
+ /** QUIC stream ID */
38
+ streamId?: number;
39
+
40
+ // WebTransport events (available after res.writeHead(200) on WT sessions)
41
+ on(event: 'stream', listener: (stream: Duplex) => void): this;
42
+ on(event: 'unidirectionalStream', listener: (stream: Readable) => void): this;
43
+ on(event: 'datagram', listener: (data: Buffer) => void): this;
44
+ on(event: string, listener: (...args: any[]) => void): this;
45
+ }
46
+
47
+ interface QuicoServerResponse extends ServerResponse {
48
+ // WebTransport methods (available after res.writeHead(200) on WT sessions)
49
+ sendDatagram?(data: Buffer | Uint8Array | string): void;
50
+ createBidirectionalStream?(): Duplex;
51
+ createUnidirectionalStream?(): Writable;
52
+ }
53
+
54
+
55
+ // ============================================================
56
+ // Client
57
+ // ============================================================
58
+
59
+ interface RequestOptions {
60
+ hostname?: string;
61
+ host?: string;
62
+ port?: number;
63
+ path?: string;
64
+ method?: string;
65
+ headers?: Record<string, string | string[]>;
66
+ agent?: QuicoAgent;
67
+ rejectUnauthorized?: boolean;
68
+ timeout?: number;
69
+ }
70
+
71
+ interface QuicoClientRequest extends Writable {
72
+ on(event: 'response', listener: (res: QuicoIncomingMessage) => void): this;
73
+ on(event: 'error', listener: (err: Error) => void): this;
74
+ on(event: 'timeout', listener: () => void): this;
75
+ on(event: string, listener: (...args: any[]) => void): this;
76
+ end(data?: string | Buffer): this;
77
+ abort(): void;
78
+ setTimeout(ms: number, callback?: () => void): this;
79
+ }
80
+
81
+
82
+ // ============================================================
83
+ // Agent
84
+ // ============================================================
85
+
86
+ interface QuicoAgent {
87
+ destroy(): void;
88
+ }
89
+
90
+
91
+ // ============================================================
92
+ // WebTransport
93
+ // ============================================================
94
+
95
+ interface WebTransportOptions {
96
+ /** Skip certificate verification (default: true for self-signed) */
97
+ rejectUnauthorized?: boolean;
98
+ }
99
+
100
+ declare class WebTransport extends EventEmitter {
101
+ constructor(url: string, options?: WebTransportOptions);
102
+
103
+ /** Resolves when the WebTransport session is established */
104
+ readonly ready: Promise<void>;
105
+ /** Current state: 'connecting' | 'connected' | 'closed' */
106
+ readonly state: string;
107
+ /** The URL of the WebTransport session */
108
+ readonly url: string;
109
+
110
+ /** WHATWG-style datagram interface */
111
+ readonly datagrams: {
112
+ readonly readable: ReadableStream<Uint8Array>;
113
+ readonly writable: WritableStream<Uint8Array>;
114
+ };
115
+
116
+ /** Create a bidirectional stream (Node.js Duplex + WHATWG .readable/.writable) */
117
+ createBidirectionalStream(): Promise<Duplex & {
118
+ readonly readable: ReadableStream<Uint8Array>;
119
+ readonly writable: WritableStream<Uint8Array>;
120
+ }>;
121
+
122
+ /** Create a unidirectional stream for writing */
123
+ createUnidirectionalStream(): Promise<Writable>;
124
+
125
+ /** Send an unreliable datagram */
126
+ sendDatagram(data: Buffer | Uint8Array): void;
127
+
128
+ /** Close the WebTransport session */
129
+ close(info?: { closeCode?: number; reason?: string }): void;
130
+
131
+ on(event: 'datagram', listener: (data: Buffer) => void): this;
132
+ on(event: 'close', listener: () => void): this;
133
+ on(event: string, listener: (...args: any[]) => void): this;
134
+ }
135
+
136
+
137
+ // ============================================================
138
+ // Main module
139
+ // ============================================================
140
+
141
+ declare const quico: {
142
+ createServer(options: ServerOptions, handler?: RequestHandler): QuicoServer;
143
+
144
+ request(url: string | URL, callback?: (res: QuicoIncomingMessage) => void): QuicoClientRequest;
145
+ request(url: string | URL, options: RequestOptions, callback?: (res: QuicoIncomingMessage) => void): QuicoClientRequest;
146
+ request(options: RequestOptions, callback?: (res: QuicoIncomingMessage) => void): QuicoClientRequest;
147
+
148
+ get(url: string | URL, callback?: (res: QuicoIncomingMessage) => void): QuicoClientRequest;
149
+ get(url: string | URL, options: RequestOptions, callback?: (res: QuicoIncomingMessage) => void): QuicoClientRequest;
150
+ get(options: RequestOptions, callback?: (res: QuicoIncomingMessage) => void): QuicoClientRequest;
151
+
152
+ globalAgent: QuicoAgent;
153
+ };
154
+
155
+ export default quico;
156
+ export { WebTransport };
157
+ export { QuicoServer, QuicoIncomingMessage, QuicoServerResponse, QuicoClientRequest, QuicoAgent };
158
+ export { ServerOptions, RequestOptions, RequestHandler, WebTransportOptions };
package/index.js CHANGED
@@ -1,23 +1,49 @@
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
- * Unless required by applicable law or agreed to in writing, software
15
- * distributed under the License is distributed on an "AS IS" BASIS,
16
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
- */
18
-
19
- import { createServer } from './h3_server.js';
20
-
21
- export { createServer };
22
-
23
- export default { createServer };
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 { createServer } from './src/server.js';
16
+ import { request, get } from './src/client.js';
17
+ import { Agent, globalAgent } from './src/agent.js';
18
+ import { IncomingMessage, ServerResponse, ClientRequest } from './src/streams.js';
19
+ import { createSecureContext } from './src/tls_bridge.js';
20
+ import { WebTransport } from './src/webtransport.js';
21
+ import { createQuicServer } from './src/quic_server.js';
22
+
23
+ export {
24
+ createServer,
25
+ request,
26
+ get,
27
+ Agent,
28
+ globalAgent,
29
+ IncomingMessage,
30
+ ServerResponse,
31
+ ClientRequest,
32
+ createSecureContext,
33
+ WebTransport,
34
+ createQuicServer
35
+ };
36
+
37
+ export default {
38
+ createServer,
39
+ request,
40
+ get,
41
+ Agent,
42
+ globalAgent,
43
+ IncomingMessage,
44
+ ServerResponse,
45
+ ClientRequest,
46
+ createSecureContext,
47
+ WebTransport,
48
+ createQuicServer
49
+ };
package/package.json CHANGED
@@ -1,60 +1,83 @@
1
- {
2
- "name": "quico",
3
- "version": "0.1.3",
4
- "description": "A pure JavaScript implementation of QUIC, HTTP/3, QPACK, and WebTransport for Node.js",
5
- "main": "index.js",
6
- "type": "module",
7
- "keywords": [
8
- "quic",
9
- "http3",
10
- "http",
11
- "https",
12
- "h3",
13
- "quic-node",
14
- "http3-node",
15
- "quic-server",
16
- "http3-server",
17
- "h3-node",
18
- "webtransport",
19
- "websocket",
20
- "datagram",
21
- "udp",
22
- "qpack",
23
- "tls",
24
- "tls1.3",
25
- "tls13",
26
- "nodejs",
27
- "backend",
28
- "real-time",
29
- "low-latency",
30
- "transport",
31
- "http-server",
32
- "socketio"
33
- ],
34
- "author": "colocohen",
35
- "license": "Apache-2.0",
36
- "repository": {
37
- "type": "git",
38
- "url": "https://github.com/colocohen/quico.git"
39
- },
40
- "bugs": {
41
- "url": "https://github.com/colocohen/quico/issues"
42
- },
43
- "homepage": "https://github.com/colocohen/quico#readme",
44
- "funding": [
45
- {
46
- "type": "github",
47
- "url": "https://github.com/sponsors/colocohen"
48
- },
49
- {
50
- "type": "opencollective",
51
- "url": "https://opencollective.com/quico-node"
52
- }
53
- ],
54
- "dependencies": {
55
- "@noble/curves": "^2.0.1",
56
- "@noble/hashes": "^2.0.1",
57
- "flat-ranges": "^1.0.2",
58
- "lemon-tls": "^0.1.1"
59
- }
60
- }
1
+ {
2
+ "name": "quico",
3
+ "version": "0.3.0",
4
+ "description": "A pure JavaScript implementation of QUIC, HTTP/3, QPACK, and WebTransport for Node.js",
5
+ "type": "module",
6
+ "main": "index.cjs",
7
+ "module": "index.js",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./index.js",
11
+ "require": "./index.cjs"
12
+ }
13
+ },
14
+ "engines": {
15
+ "node": ">=18.0.0"
16
+ },
17
+ "types": "index.d.ts",
18
+ "files": [
19
+ "index.js",
20
+ "index.cjs",
21
+ "index.d.ts",
22
+ "src/",
23
+ "LICENSE",
24
+ "README.md"
25
+ ],
26
+ "keywords": [
27
+ "quic",
28
+ "http3",
29
+ "http",
30
+ "https",
31
+ "h3",
32
+ "quic-node",
33
+ "http3-node",
34
+ "quic-server",
35
+ "quic-client",
36
+ "http3-server",
37
+ "http3-client",
38
+ "h3-node",
39
+ "webtransport",
40
+ "websocket",
41
+ "datagram",
42
+ "udp",
43
+ "qpack",
44
+ "tls",
45
+ "tls1.3",
46
+ "tls13",
47
+ "nodejs",
48
+ "backend",
49
+ "real-time",
50
+ "low-latency",
51
+ "transport",
52
+ "http-server",
53
+ "socketio"
54
+ ],
55
+ "author": {
56
+ "name": "colocohen",
57
+ "email": "support@quicojs.dev"
58
+ },
59
+ "license": "Apache-2.0",
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "https://github.com/colocohen/quico.git"
63
+ },
64
+ "bugs": {
65
+ "email": "support@quicojs.dev",
66
+ "url": "https://github.com/colocohen/quico/issues"
67
+ },
68
+ "homepage": "https://github.com/colocohen/quico#readme",
69
+ "funding": [
70
+ {
71
+ "type": "github",
72
+ "url": "https://github.com/sponsors/colocohen"
73
+ },
74
+ {
75
+ "type": "opencollective",
76
+ "url": "https://opencollective.com/quicojs"
77
+ }
78
+ ],
79
+ "dependencies": {
80
+ "flat-ranges": "^2.0.0",
81
+ "lemon-tls": "^0.4.1"
82
+ }
83
+ }
package/src/agent.js ADDED
@@ -0,0 +1,283 @@
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 { EventEmitter } from 'node:events';
16
+
17
+
18
+ // ============================================================
19
+ // Agent — connection pooling + protocol discovery
20
+ //
21
+ // Mirrors node:https.Agent API surface.
22
+ // Caches which protocol works per host so subsequent requests
23
+ // skip the discovery step.
24
+ //
25
+ // Options:
26
+ // http2: true (default) — allow HTTP/2 in fallback chain
27
+ // http1: true (default) — allow HTTP/1.1 in fallback chain
28
+ // h3Timeout: 3000 — ms before falling back from H3
29
+ // ============================================================
30
+
31
+ class Agent extends EventEmitter {
32
+
33
+ constructor(options) {
34
+ super();
35
+
36
+ options = options || {};
37
+
38
+ this.keepAlive = options.keepAlive !== false;
39
+ this.keepAliveMsecs = options.keepAliveMsecs || 1000;
40
+ this.maxSockets = options.maxSockets || 256;
41
+ this.maxFreeSockets = options.maxFreeSockets || 32;
42
+ this.timeout = options.timeout || 30000;
43
+
44
+ // Protocol flags (defaults for all requests through this agent)
45
+ this.http2 = options.http2 !== false;
46
+ this.http1 = options.http1 !== false;
47
+
48
+ // H3 connect timeout before falling back (ms)
49
+ this.h3Timeout = options.h3Timeout || 3000;
50
+
51
+ // Protocol cache: "host:port" → { protocol, ts, altSvcMaxAge }
52
+ // protocol: 'h3' | 'h2' | 'https'
53
+ this._protocolCache = {};
54
+
55
+ // Active QUIC connections: "host:port" → QUICConnection
56
+ // Allows multiplexing multiple requests over one QUIC connection
57
+ this._h3Pool = {};
58
+
59
+ // Active HTTP/2 sessions: "host:port" → http2.ClientHttp2Session
60
+ this._h2Pool = {};
61
+
62
+ // Request count per host (for maxSockets enforcement)
63
+ this._activeCounts = {};
64
+
65
+ // Sweep interval — clean stale pool entries
66
+ this._sweepTimer = setInterval(() => this._sweep(), 15000);
67
+ if (this._sweepTimer.unref) this._sweepTimer.unref();
68
+ }
69
+
70
+
71
+ // ============================================================
72
+ // Protocol cache
73
+ // ============================================================
74
+
75
+ /**
76
+ * Get cached protocol for a host, or null if unknown.
77
+ */
78
+ getProtocol(host, port) {
79
+ var key = host + ':' + (port || 443);
80
+ var entry = this._protocolCache[key];
81
+ if (!entry) return null;
82
+
83
+ // Check expiry (Alt-Svc max-age, default 24h)
84
+ var maxAge = entry.altSvcMaxAge || 86400000;
85
+ if (Date.now() - entry.ts > maxAge) {
86
+ delete this._protocolCache[key];
87
+ return null;
88
+ }
89
+
90
+ return entry.protocol;
91
+ }
92
+
93
+ /**
94
+ * Record which protocol works for a host.
95
+ */
96
+ setProtocol(host, port, protocol, altSvcMaxAge) {
97
+ var key = host + ':' + (port || 443);
98
+ this._protocolCache[key] = {
99
+ protocol: protocol,
100
+ ts: Date.now(),
101
+ altSvcMaxAge: altSvcMaxAge || 86400000
102
+ };
103
+ }
104
+
105
+
106
+ // ============================================================
107
+ // Alt-Svc header parsing
108
+ // ============================================================
109
+
110
+ /**
111
+ * Parse Alt-Svc header from an HTTP/1.1 or HTTP/2 response.
112
+ * If it advertises h3, cache it for future requests.
113
+ *
114
+ * Examples:
115
+ * Alt-Svc: h3=":443"; ma=86400
116
+ * Alt-Svc: h3=":443", h3-29=":443"
117
+ */
118
+ parseAltSvc(host, port, altSvcValue) {
119
+ if (!altSvcValue || altSvcValue === 'clear') {
120
+ var key = host + ':' + (port || 443);
121
+ delete this._protocolCache[key];
122
+ return;
123
+ }
124
+
125
+ var entries = altSvcValue.split(',');
126
+ for (var i = 0; i < entries.length; i++) {
127
+ var entry = entries[i].trim();
128
+ var match = entry.match(/^h3(?:-\d+)?="([^"]*)"(.*)$/);
129
+ if (match) {
130
+ var authority = match[1];
131
+ var params = match[2] || '';
132
+
133
+ var maMatch = params.match(/ma=(\d+)/);
134
+ var maxAge = maMatch ? parseInt(maMatch[1]) * 1000 : 86400000;
135
+
136
+ var h3Port = port;
137
+ var colonIdx = authority.lastIndexOf(':');
138
+ if (colonIdx >= 0) {
139
+ var parsed = parseInt(authority.substring(colonIdx + 1));
140
+ if (!isNaN(parsed)) h3Port = parsed;
141
+ }
142
+
143
+ this.setProtocol(host, h3Port, 'h3', maxAge);
144
+ return;
145
+ }
146
+ }
147
+ }
148
+
149
+
150
+ // ============================================================
151
+ // H3 connection pool
152
+ // Stores { quic, h3, udpSocket, _nextStreamId } objects
153
+ // from h3_client.js for multiplexing.
154
+ // ============================================================
155
+
156
+ getH3Connection(host, port) {
157
+ var key = host + ':' + (port || 443);
158
+ var conn = this._h3Pool[key];
159
+ if (!conn) return null;
160
+ var state = conn.quic ? conn.quic.state : 'closed';
161
+ if (state === 'closed' || state === 'draining' || state === 'closing') {
162
+ delete this._h3Pool[key];
163
+ return null;
164
+ }
165
+ return conn;
166
+ }
167
+
168
+ setH3Connection(host, port, conn) {
169
+ var key = host + ':' + (port || 443);
170
+ this._h3Pool[key] = conn;
171
+ if (conn.quic) {
172
+ conn.quic.on('close', () => {
173
+ if (this._h3Pool[key] === conn) delete this._h3Pool[key];
174
+ });
175
+ }
176
+ // RFC 9114 §5.2: once the peer sends GOAWAY it won't accept new requests, so
177
+ // stop reusing this connection for new ones. In-flight requests keep running
178
+ // on it; they just won't be joined by new requests (which open a fresh conn).
179
+ if (conn.h3 && typeof conn.h3.on === 'function') {
180
+ conn.h3.on('goaway', () => {
181
+ if (this._h3Pool[key] === conn) delete this._h3Pool[key];
182
+ });
183
+ }
184
+ }
185
+
186
+
187
+ // ============================================================
188
+ // H2 session pool
189
+ // ============================================================
190
+
191
+ getH2Session(host, port) {
192
+ var key = host + ':' + (port || 443);
193
+ var session = this._h2Pool[key];
194
+ if (!session) return null;
195
+ if (session.closed || session.destroyed) {
196
+ delete this._h2Pool[key];
197
+ return null;
198
+ }
199
+ return session;
200
+ }
201
+
202
+ setH2Session(host, port, session) {
203
+ var key = host + ':' + (port || 443);
204
+ this._h2Pool[key] = session;
205
+ session.on('close', () => {
206
+ if (this._h2Pool[key] === session) delete this._h2Pool[key];
207
+ });
208
+ session.on('error', () => {
209
+ if (this._h2Pool[key] === session) delete this._h2Pool[key];
210
+ });
211
+ }
212
+
213
+
214
+ // ============================================================
215
+ // Active request tracking
216
+ // ============================================================
217
+
218
+ canCreateSocket(host, port) {
219
+ var key = host + ':' + (port || 443);
220
+ return (this._activeCounts[key] || 0) < this.maxSockets;
221
+ }
222
+
223
+ trackRequest(host, port) {
224
+ var key = host + ':' + (port || 443);
225
+ this._activeCounts[key] = (this._activeCounts[key] || 0) + 1;
226
+ }
227
+
228
+ releaseRequest(host, port) {
229
+ var key = host + ':' + (port || 443);
230
+ if (this._activeCounts[key]) {
231
+ this._activeCounts[key]--;
232
+ if (this._activeCounts[key] <= 0) delete this._activeCounts[key];
233
+ }
234
+ }
235
+
236
+
237
+ // ============================================================
238
+ // Sweep stale entries
239
+ // ============================================================
240
+
241
+ _sweep() {
242
+ var now = Date.now();
243
+
244
+ for (var key in this._protocolCache) {
245
+ var entry = this._protocolCache[key];
246
+ var maxAge = entry.altSvcMaxAge || 86400000;
247
+ if (now - entry.ts > maxAge) delete this._protocolCache[key];
248
+ }
249
+
250
+ for (var key in this._h3Pool) {
251
+ var conn = this._h3Pool[key];
252
+ var state = conn.quic ? conn.quic.state : 'closed';
253
+ if (state === 'closed' || state === 'draining') delete this._h3Pool[key];
254
+ }
255
+
256
+ for (var key in this._h2Pool) {
257
+ var session = this._h2Pool[key];
258
+ if (session.closed || session.destroyed) delete this._h2Pool[key];
259
+ }
260
+ }
261
+
262
+ destroy() {
263
+ if (this._sweepTimer) { clearInterval(this._sweepTimer); this._sweepTimer = null; }
264
+
265
+ for (var key in this._h3Pool) {
266
+ try { this._h3Pool[key].quic.close(0, 'agent destroy'); } catch (e) {}
267
+ }
268
+ this._h3Pool = {};
269
+
270
+ for (var key in this._h2Pool) {
271
+ try { this._h2Pool[key].close(); } catch (e) {}
272
+ }
273
+ this._h2Pool = {};
274
+
275
+ this._protocolCache = {};
276
+ this._activeCounts = {};
277
+ }
278
+ }
279
+
280
+
281
+ var globalAgent = new Agent();
282
+
283
+ export { Agent, globalAgent };