@afterlink/server 1.0.0 → 1.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afterlink/server",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "AfterLink Protocol Server SDK - TCP server with routing, middleware, and pub/sub",
5
5
  "main": "src/index.js",
6
6
  "license": "MIT",
@@ -29,7 +29,8 @@
29
29
  "test": "vitest run"
30
30
  },
31
31
  "dependencies": {
32
- "@afterlink/core": "1.0.0"
32
+ "@afterlink/core": "^1.1.0",
33
+ "selfsigned": "^5.5.0"
33
34
  },
34
35
  "devDependencies": {
35
36
  "vitest": "^1.6.0"
package/src/Connection.js CHANGED
@@ -2,6 +2,7 @@ const {
2
2
  Frame,
3
3
  FrameTypes: { HELLO, HELLO_ACK, ERROR },
4
4
  Serializer,
5
+ compression,
5
6
  } = require('@afterlink/core');
6
7
  const FrameAccumulator = require('./FrameAccumulator');
7
8
 
@@ -14,6 +15,20 @@ class Connection {
14
15
  this._id = `conn_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
15
16
  this._closed = false;
16
17
 
18
+ // Compression state (negotiated during handshake)
19
+ this._compression = {
20
+ enabled: false,
21
+ algorithm: 'none',
22
+ level: 6,
23
+ threshold: 1024,
24
+ };
25
+
26
+ // Rate limiting bucket (attached by server if enabled)
27
+ this._rateBucket = null;
28
+
29
+ // Active request tracking for graceful shutdown
30
+ this._activeRequests = new Set();
31
+
17
32
  try {
18
33
  this.accumulator = new FrameAccumulator(this._handleFrame.bind(this));
19
34
  } catch (err) {
@@ -56,7 +71,28 @@ class Connection {
56
71
  if (frame.type === HELLO && !this.session) {
57
72
  this._handleHandshake(frame);
58
73
  } else if (this.session) {
74
+ // Decompress incoming frame if compressed
75
+ if (compression.isCompressed(frame.flags)) {
76
+ try {
77
+ frame.payload = compression.decompress(
78
+ frame.payload,
79
+ true,
80
+ this._compression.algorithm
81
+ );
82
+ } catch (err) {
83
+ this.sendError('DECOMPRESSION_ERROR', 'Failed to decompress payload');
84
+ return;
85
+ }
86
+ }
87
+
88
+ // Track active requests for graceful shutdown
89
+ const { REQUEST } = require('@afterlink/core').FrameTypes;
90
+ if (frame.type === REQUEST) {
91
+ this._activeRequests.add(frame.messageId);
92
+ }
93
+
59
94
  this.router.dispatch(frame, this).catch((err) => {
95
+ this._activeRequests.delete(frame.messageId);
60
96
  this._onError(err);
61
97
  });
62
98
  } else {
@@ -74,18 +110,49 @@ class Connection {
74
110
  this._validateAuth(data.auth);
75
111
  }
76
112
 
113
+ // Negotiate compression
114
+ const serverCompression = this.options.compression || {};
115
+ const clientAlgorithm = data.compression || 'none';
116
+ const serverEnabled = serverCompression.enabled !== false;
117
+ const serverAlgorithm = serverCompression.algorithm || 'zlib';
118
+
119
+ // Use client's preferred algorithm if server supports it
120
+ let agreedAlgorithm = 'none';
121
+ if (serverEnabled && clientAlgorithm !== 'none') {
122
+ if (clientAlgorithm === serverAlgorithm || clientAlgorithm === 'brotli') {
123
+ agreedAlgorithm = clientAlgorithm;
124
+ } else if (serverAlgorithm !== 'none') {
125
+ agreedAlgorithm = serverAlgorithm;
126
+ }
127
+ }
128
+
129
+ this._compression = {
130
+ enabled: agreedAlgorithm !== 'none',
131
+ algorithm: agreedAlgorithm,
132
+ level: serverCompression.level ?? 6,
133
+ threshold: serverCompression.threshold ?? 1024,
134
+ };
135
+
77
136
  this.session = {
78
137
  id: sessionId,
79
138
  version: data.version || 'AL/1',
80
139
  capabilities: data.capabilities || [],
81
140
  connectedAt: new Date().toISOString(),
82
141
  remoteAddress: this.getRemoteAddress(),
142
+ compression: agreedAlgorithm,
83
143
  };
84
144
 
85
145
  const ackPayload = Serializer.encode({
86
146
  session_id: sessionId,
87
- server_version: 'AL/1',
88
- capabilities: ['streaming', 'pubsub', 'compression'],
147
+ server_version: 'AL/1.1',
148
+ capabilities: ['streaming', 'pubsub', 'compression', 'rate-limit'],
149
+ compression: agreedAlgorithm,
150
+ rateLimit: this.options.rateLimit?.enabled
151
+ ? {
152
+ requestsPerSecond: this.options.rateLimit.requestsPerSecond,
153
+ burstSize: this.options.rateLimit.burstSize,
154
+ }
155
+ : undefined,
89
156
  });
90
157
  this.send(HELLO_ACK, 0, frame.messageId, ackPayload);
91
158
  } catch (err) {
@@ -104,8 +171,28 @@ class Connection {
104
171
  send(type, flags, messageId, payload) {
105
172
  if (this._closed || this.socket.destroyed) return;
106
173
  try {
107
- const frame = Frame.encode(type, flags, messageId, payload);
174
+ // Compress payload if enabled and above threshold
175
+ let finalPayload = payload;
176
+ let finalFlags = flags;
177
+ if (this._compression.enabled && this.session) {
178
+ const { data, compressed } = compression.compress(
179
+ payload,
180
+ this._compression.algorithm,
181
+ this._compression.level,
182
+ this._compression.threshold
183
+ );
184
+ finalPayload = data;
185
+ finalFlags = compression.setCompressedFlag(flags, compressed);
186
+ }
187
+
188
+ const frame = Frame.encode(type, finalFlags, messageId, finalPayload);
108
189
  this.socket.write(frame);
190
+
191
+ // Track response sent - remove from active requests
192
+ const { RESPONSE, ERROR } = require('@afterlink/core').FrameTypes;
193
+ if (type === RESPONSE || type === ERROR) {
194
+ this._activeRequests.delete(messageId);
195
+ }
109
196
  } catch (err) {
110
197
  this._onError(err);
111
198
  }
package/src/Router.js CHANGED
@@ -80,7 +80,7 @@ class Router {
80
80
  }
81
81
 
82
82
  let responseSent = false;
83
- const req = { body, session: connection.session, route };
83
+ const req = { body, session: connection.session, route, connection };
84
84
  const res = {
85
85
  send: (data) => {
86
86
  if (responseSent) return;
@@ -100,7 +100,22 @@ class Router {
100
100
  });
101
101
  } catch (err) {
102
102
  if (!responseSent) {
103
- connection.sendError('INTERNAL_ERROR', err.message, messageId);
103
+ if (err.code === 'RATE_LIMITED') {
104
+ const errorPayload = Serializer.encode({
105
+ code: err.code,
106
+ message: err.message,
107
+ retryAfter: err.retryAfter,
108
+ limit: err.limit,
109
+ remaining: err.remaining,
110
+ });
111
+ connection.send(ERROR, 0, messageId, errorPayload);
112
+
113
+ if (err.closeConnection) {
114
+ connection.destroy();
115
+ }
116
+ } else {
117
+ connection.sendError('INTERNAL_ERROR', err.message, messageId);
118
+ }
104
119
  }
105
120
  }
106
121
  }
package/src/Server.js CHANGED
@@ -1,4 +1,7 @@
1
- const net = require('net');
1
+ const { createServerTransport, isTLSSocket, getTLSInfo } = require('./transport/tls');
2
+ const { TokenBucket, createRateLimitMiddleware } = require('./middleware/rate-limit');
3
+ const { GracefulShutdown } = require('./shutdown/graceful');
4
+
2
5
  const Connection = require('./Connection');
3
6
  const Router = require('./Router');
4
7
 
@@ -8,12 +11,48 @@ class Server {
8
11
  port: 4000,
9
12
  host: '0.0.0.0',
10
13
  maxConnections: 10000,
14
+ compression: {
15
+ enabled: false,
16
+ algorithm: 'zlib',
17
+ threshold: 1024,
18
+ level: 6,
19
+ },
20
+ rateLimit: {
21
+ enabled: false,
22
+ requestsPerSecond: 100,
23
+ burstSize: 200,
24
+ closeAfterViolations: null,
25
+ errorMessage: 'Rate limit exceeded. Please slow down.',
26
+ onLimited: null,
27
+ },
28
+ shutdown: {
29
+ drainTimeout: 5000,
30
+ reason: 'planned_restart',
31
+ notifyClients: true,
32
+ },
11
33
  ...config,
12
34
  };
35
+ // Merge nested configs deeply
36
+ if (config.compression) {
37
+ this.config.compression = { ...this.config.compression, ...config.compression };
38
+ }
39
+ if (config.rateLimit) {
40
+ this.config.rateLimit = { ...this.config.rateLimit, ...config.rateLimit };
41
+ }
42
+ if (config.shutdown) {
43
+ this.config.shutdown = { ...this.config.shutdown, ...config.shutdown };
44
+ }
45
+
13
46
  this.router = new Router();
14
47
  this.connections = new Set();
15
- this.tcp = null;
48
+ this.transport = null;
16
49
  this._listening = false;
50
+ this._tlsEnabled = !!config.tls?.enabled;
51
+ this._shutdown = null;
52
+ this._eventListeners = new Map();
53
+
54
+ // Initialize graceful shutdown handler
55
+ this._shutdown = new GracefulShutdown(this, this.config.shutdown);
17
56
  }
18
57
 
19
58
  on(route, handler, schema = null) {
@@ -37,51 +76,147 @@ class Server {
37
76
  return reject(new Error('Server is already listening'));
38
77
  }
39
78
 
40
- this.tcp = net.createServer((socket) => {
41
- if (this.connections.size >= this.config.maxConnections) {
42
- socket.destroy();
43
- return;
79
+ try {
80
+ this.transport = createServerTransport(this.config);
81
+ } catch (err) {
82
+ return reject(err);
83
+ }
84
+
85
+ // Auto-add rate limiting middleware if enabled
86
+ if (this.config.rateLimit.enabled) {
87
+ const rateLimitMiddleware = createRateLimitMiddleware(this.config.rateLimit);
88
+ if (rateLimitMiddleware) {
89
+ this.router.addMiddleware(rateLimitMiddleware);
44
90
  }
91
+ }
45
92
 
46
- socket.setKeepAlive(true, 60000);
47
- socket.setNoDelay(true);
93
+ this.transport.on('secureConnection', (socket) => {
94
+ this._handleConnection(socket);
95
+ });
48
96
 
49
- const conn = new Connection(socket, this.router, {
50
- auth: this.config.auth,
51
- });
52
- this.connections.add(conn);
53
- socket.on('close', () => this.connections.delete(conn));
97
+ this.transport.on('connection', (socket) => {
98
+ // For TLS servers, skip the raw connection event (handled by secureConnection)
99
+ if (this._tlsEnabled) return;
100
+ this._handleConnection(socket);
54
101
  });
55
102
 
56
- this.tcp.on('error', (err) => {
103
+ this.transport.on('error', (err) => {
57
104
  if (err.code === 'EADDRINUSE') {
58
105
  reject(new Error(`Port ${port} is already in use`));
106
+ } else if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
107
+ const tlsErr = new Error('TLS certificate does not match hostname');
108
+ tlsErr.code = 'TLS_CERT_ERROR';
109
+ reject(tlsErr);
59
110
  } else {
60
111
  console.error('[AfterLink] Server error:', err.message);
61
112
  }
62
113
  });
63
114
 
64
- this.tcp.listen(port, this.config.host, () => {
115
+ this.transport.listen(port, this.config.host, () => {
65
116
  this._listening = true;
66
- console.log(`[AfterLink] Server listening on ${this.config.host}:${port}`);
117
+ const proto = this._tlsEnabled ? 'TLS' : 'TCP';
118
+ console.log(`[AfterLink] ${proto} Server listening on ${this.config.host}:${port}`);
67
119
  resolve(this);
68
120
  });
69
121
  });
70
122
  }
71
123
 
72
- close() {
124
+ _handleConnection(socket) {
125
+ if (this.connections.size >= this.config.maxConnections) {
126
+ socket.destroy();
127
+ return;
128
+ }
129
+
130
+ socket.setKeepAlive(true, 60000);
131
+ socket.setNoDelay(true);
132
+
133
+ const conn = new Connection(socket, this.router, {
134
+ auth: this.config.auth,
135
+ compression: this.config.compression,
136
+ rateLimit: this.config.rateLimit,
137
+ });
138
+
139
+ // Attach rate bucket if rate limiting is enabled
140
+ if (this.config.rateLimit.enabled) {
141
+ const { requestsPerSecond, burstSize } = this.config.rateLimit;
142
+ conn._rateBucket = new TokenBucket(burstSize, requestsPerSecond / 1000);
143
+ }
144
+
145
+ // Attach TLS info if available
146
+ if (isTLSSocket(socket)) {
147
+ conn.tlsInfo = getTLSInfo(socket);
148
+ }
149
+
150
+ this.connections.add(conn);
151
+ socket.on('close', () => this.connections.delete(conn));
152
+ }
153
+
154
+ on(routeOrEvent, handlerOrListener, schema = null) {
155
+ // Check if this is route registration (handler is a function with route pattern)
156
+ if (typeof routeOrEvent === 'string' && typeof handlerOrListener === 'function' && !this._eventListeners.has(routeOrEvent)) {
157
+ // Route registration
158
+ this.router.register(routeOrEvent, handlerOrListener, schema);
159
+ return this;
160
+ }
161
+
162
+ // Event listener
163
+ const event = routeOrEvent;
164
+ const listener = handlerOrListener;
165
+ if (!this._eventListeners.has(event)) {
166
+ this._eventListeners.set(event, new Set());
167
+ }
168
+ this._eventListeners.get(event).add(listener);
169
+ return this;
170
+ }
171
+
172
+ off(event, listener) {
173
+ const listeners = this._eventListeners.get(event);
174
+ if (listeners) {
175
+ listeners.delete(listener);
176
+ }
177
+ return this;
178
+ }
179
+
180
+ _emit(event, data) {
181
+ const listeners = this._eventListeners.get(event);
182
+ if (listeners) {
183
+ for (const listener of listeners) {
184
+ try {
185
+ listener(data);
186
+ } catch (err) {
187
+ console.error(`[AfterLink] Event listener error for '${event}':`, err.message);
188
+ }
189
+ }
190
+ }
191
+ }
192
+
193
+ async close(options = {}) {
73
194
  return new Promise((resolve) => {
74
- if (!this.tcp || !this._listening) {
195
+ if (!this.transport || !this._listening) {
75
196
  return resolve();
76
197
  }
77
198
 
78
199
  this._listening = false;
79
200
 
80
- for (const conn of this.connections) {
81
- conn.destroy();
201
+ if (options.force) {
202
+ // Force close - skip drain
203
+ for (const conn of this.connections) {
204
+ conn.destroy();
205
+ }
206
+ this.transport.close(() => {
207
+ this._emit('closed');
208
+ resolve();
209
+ });
210
+ return;
82
211
  }
83
212
 
84
- this.tcp.close(() => resolve());
213
+ // Graceful shutdown
214
+ this._shutdown.initiate().then(() => {
215
+ this.transport.close(() => {
216
+ this._emit('closed');
217
+ resolve();
218
+ });
219
+ });
85
220
  });
86
221
  }
87
222
 
@@ -96,6 +231,26 @@ class Server {
96
231
  isListening() {
97
232
  return this._listening;
98
233
  }
234
+
235
+ isTLS() {
236
+ return this._tlsEnabled;
237
+ }
238
+
239
+ handleProcessSignals() {
240
+ process.on('SIGTERM', async () => {
241
+ console.log('[AfterLink] SIGTERM received — shutting down gracefully');
242
+ await this.close();
243
+ process.exit(0);
244
+ });
245
+
246
+ process.on('SIGINT', async () => {
247
+ console.log('[AfterLink] SIGINT received — shutting down gracefully');
248
+ await this.close();
249
+ process.exit(0);
250
+ });
251
+
252
+ return this;
253
+ }
99
254
  }
100
255
 
101
256
  module.exports = Server;
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  const Server = require('./Server');
2
+ const { generateDevCerts } = require('./tls/dev-certs');
2
3
 
3
- module.exports = { Server };
4
+ module.exports = { Server, generateDevCerts };
@@ -0,0 +1,81 @@
1
+ class TokenBucket {
2
+ constructor(capacity, refillRate) {
3
+ this.capacity = capacity;
4
+ this.refillRate = refillRate;
5
+ this.tokens = capacity;
6
+ this.lastRefill = Date.now();
7
+ this.violations = 0;
8
+ }
9
+
10
+ consume() {
11
+ this._refill();
12
+ if (this.tokens < 1) {
13
+ this.violations++;
14
+ return {
15
+ allowed: false,
16
+ retryAfter: Math.ceil((1 - this.tokens) / this.refillRate),
17
+ };
18
+ }
19
+ this.tokens--;
20
+ return { allowed: true, retryAfter: 0 };
21
+ }
22
+
23
+ _refill() {
24
+ const now = Date.now();
25
+ const elapsed = now - this.lastRefill;
26
+ this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate);
27
+ this.lastRefill = now;
28
+ }
29
+ }
30
+
31
+ function createRateLimitMiddleware(options = {}) {
32
+ const {
33
+ enabled = false,
34
+ requestsPerSecond = 100,
35
+ burstSize = 200,
36
+ closeAfterViolations = null,
37
+ errorMessage = 'Rate limit exceeded. Please slow down.',
38
+ onLimited = null,
39
+ } = options;
40
+
41
+ if (!enabled) {
42
+ return null;
43
+ }
44
+
45
+ const refillRatePerMs = requestsPerSecond / 1000;
46
+
47
+ return function rateLimitMiddleware(req, next) {
48
+ const bucket = req.connection._rateBucket;
49
+ if (!bucket) {
50
+ return next();
51
+ }
52
+
53
+ const result = bucket.consume();
54
+
55
+ if (!result.allowed) {
56
+ const err = new Error(errorMessage);
57
+ err.code = 'RATE_LIMITED';
58
+ err.retryAfter = result.retryAfter;
59
+ err.limit = requestsPerSecond;
60
+ err.remaining = 0;
61
+
62
+ if (onLimited) {
63
+ try {
64
+ onLimited(req.connection);
65
+ } catch {
66
+ // Ignore callback errors
67
+ }
68
+ }
69
+
70
+ if (closeAfterViolations && bucket.violations >= closeAfterViolations) {
71
+ err.closeConnection = true;
72
+ }
73
+
74
+ throw err;
75
+ }
76
+
77
+ return next();
78
+ };
79
+ }
80
+
81
+ module.exports = { TokenBucket, createRateLimitMiddleware };
@@ -0,0 +1,95 @@
1
+ const { Frame, Serializer } = require('@afterlink/core');
2
+
3
+ const SERVER_CLOSING = 0x11;
4
+
5
+ class GracefulShutdown {
6
+ constructor(server, config = {}) {
7
+ this.server = server;
8
+ this.drainTimeout = config.drainTimeout ?? 5000;
9
+ this.reason = config.reason ?? 'planned_restart';
10
+ this.notifyClients = config.notifyClients !== false;
11
+ this._closing = false;
12
+ this._closed = false;
13
+ }
14
+
15
+ async initiate() {
16
+ if (this._closing || this._closed) {
17
+ return;
18
+ }
19
+
20
+ this._closing = true;
21
+
22
+ const activeConnections = this.server.connections;
23
+ const activeRequests = this._countActiveRequests(activeConnections);
24
+
25
+ this.server._emit('closing', {
26
+ activeConnections: activeConnections.size,
27
+ activeRequests,
28
+ });
29
+
30
+ if (this.notifyClients) {
31
+ this._broadcastClosing(activeConnections);
32
+ }
33
+
34
+ const drainStart = Date.now();
35
+ await this._waitForDrain(activeConnections);
36
+
37
+ const drainElapsed = Date.now() - drainStart;
38
+ if (drainElapsed >= this.drainTimeout) {
39
+ this.server._emit('drained', { timedOut: true });
40
+ } else {
41
+ this.server._emit('drained', { timedOut: false });
42
+ }
43
+
44
+ this._forceCloseRemaining(activeConnections);
45
+ this._closed = true;
46
+ this._closing = false;
47
+ }
48
+
49
+ _countActiveRequests(connections) {
50
+ let total = 0;
51
+ for (const conn of connections) {
52
+ total += conn._activeRequests?.size ?? 0;
53
+ }
54
+ return total;
55
+ }
56
+
57
+ _broadcastClosing(connections) {
58
+ const payload = Serializer.encode({
59
+ drainTimeout: this.drainTimeout,
60
+ reason: this.reason,
61
+ });
62
+
63
+ for (const conn of connections) {
64
+ try {
65
+ conn.send(SERVER_CLOSING, 0, 0, payload);
66
+ } catch {
67
+ // Ignore send errors during shutdown
68
+ }
69
+ }
70
+ }
71
+
72
+ async _waitForDrain(connections) {
73
+ const deadline = Date.now() + this.drainTimeout;
74
+
75
+ while (Date.now() < deadline) {
76
+ const active = this._countActiveRequests(connections);
77
+ if (active === 0) {
78
+ return;
79
+ }
80
+ await this._sleep(100);
81
+ }
82
+ }
83
+
84
+ _forceCloseRemaining(connections) {
85
+ for (const conn of connections) {
86
+ conn.destroy();
87
+ }
88
+ }
89
+
90
+ _sleep(ms) {
91
+ return new Promise((resolve) => setTimeout(resolve, ms));
92
+ }
93
+ }
94
+
95
+ module.exports = { GracefulShutdown, SERVER_CLOSING };
@@ -0,0 +1,33 @@
1
+ const selfsigned = require('selfsigned');
2
+
3
+ /**
4
+ * Generates self-signed development certificates.
5
+ * Uses the `selfsigned` package for reliable X.509 generation.
6
+ * NEVER use these in production.
7
+ *
8
+ * @param {object} [options]
9
+ * @param {string} [options.commonName='afterlink-dev'] - Certificate common name
10
+ * @param {number} [options.days=365] - Certificate validity in days
11
+ * @returns {Promise<{key: Buffer, cert: Buffer}>}
12
+ */
13
+ async function generateDevCerts(options = {}) {
14
+ const {
15
+ commonName = 'afterlink-dev',
16
+ days = 365,
17
+ } = options;
18
+
19
+ const attrs = [{ name: 'commonName', value: commonName }];
20
+
21
+ const pems = await selfsigned.generate(attrs, {
22
+ days,
23
+ algorithm: 'sha256',
24
+ keySize: 2048,
25
+ });
26
+
27
+ return {
28
+ key: Buffer.from(pems.private),
29
+ cert: Buffer.from(pems.cert),
30
+ };
31
+ }
32
+
33
+ module.exports = { generateDevCerts };
@@ -0,0 +1,86 @@
1
+ const tls = require('tls');
2
+ const net = require('net');
3
+
4
+ /**
5
+ * Creates a server transport (TCP or TLS) based on configuration.
6
+ * @param {object} options - Server configuration
7
+ * @param {object} [options.tls] - TLS configuration
8
+ * @param {boolean} [options.tls.enabled=false] - Enable TLS encryption
9
+ * @param {Buffer|string} [options.tls.key] - Private key PEM buffer or path
10
+ * @param {Buffer|string} [options.tls.cert] - Certificate PEM buffer or path
11
+ * @param {Buffer|string} [options.tls.ca] - CA certificate for mutual TLS
12
+ * @param {boolean} [options.tls.rejectUnauthorized=true] - Reject unauthorized clients
13
+ * @param {string} [options.tls.minVersion='TLSv1.2'] - Minimum TLS version
14
+ * @param {number} [options.tls.sessionTimeout=300] - TLS session cache timeout (seconds)
15
+ * @returns {tls.Server|net.Server}
16
+ */
17
+ function createServerTransport(options) {
18
+ if (options.tls?.enabled) {
19
+ validateTlsConfig(options.tls);
20
+
21
+ return tls.createServer({
22
+ key: options.tls.key,
23
+ cert: options.tls.cert,
24
+ ca: options.tls.ca,
25
+ requestCert: !!options.tls.ca,
26
+ rejectUnauthorized: options.tls.rejectUnauthorized ?? true,
27
+ minVersion: options.tls.minVersion ?? 'TLSv1.2',
28
+ sessionTimeout: options.tls.sessionTimeout ?? 300,
29
+ });
30
+ }
31
+
32
+ return net.createServer();
33
+ }
34
+
35
+ /**
36
+ * Validates TLS configuration and throws descriptive errors.
37
+ * @param {object} tlsConfig
38
+ * @throws {Error} TLS_CONFIG_ERROR if configuration is invalid
39
+ */
40
+ function validateTlsConfig(tlsConfig) {
41
+ if (!tlsConfig.key) {
42
+ const err = new Error('TLS enabled but no private key provided');
43
+ err.code = 'TLS_CONFIG_ERROR';
44
+ throw err;
45
+ }
46
+
47
+ if (!tlsConfig.cert) {
48
+ const err = new Error('TLS enabled but no certificate provided');
49
+ err.code = 'TLS_CONFIG_ERROR';
50
+ throw err;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Checks if a socket is a TLS socket.
56
+ * @param {net.Socket|tls.TLSSocket} socket
57
+ * @returns {boolean}
58
+ */
59
+ function isTLSSocket(socket) {
60
+ return socket instanceof tls.TLSSocket;
61
+ }
62
+
63
+ /**
64
+ * Gets TLS connection info from a socket.
65
+ * @param {net.Socket|tls.TLSSocket} socket
66
+ * @returns {object|null} TLS info or null if not TLS
67
+ */
68
+ function getTLSInfo(socket) {
69
+ if (!isTLSSocket(socket)) return null;
70
+
71
+ const tlsSocket = socket;
72
+ return {
73
+ encrypted: true,
74
+ protocol: tlsSocket.getProtocol(),
75
+ cipher: tlsSocket.getCipher(),
76
+ authorized: tlsSocket.authorized,
77
+ authorizationError: tlsSocket.authorizationError,
78
+ };
79
+ }
80
+
81
+ module.exports = {
82
+ createServerTransport,
83
+ validateTlsConfig,
84
+ isTLSSocket,
85
+ getTLSInfo,
86
+ };