novac 2.0.1 → 2.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.
Files changed (161) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1574 -597
  3. package/bin/novac +468 -171
  4. package/bin/nvc +522 -0
  5. package/bin/nvml +78 -17
  6. package/demo.nv +0 -0
  7. package/demo_builtins.nv +0 -0
  8. package/demo_http.nv +0 -0
  9. package/examples/bf.nv +69 -0
  10. package/examples/math.nv +21 -0
  11. package/kits/birdAPI/kitdef.js +954 -0
  12. package/kits/kitRNG/kitdef.js +740 -0
  13. package/kits/kitSSH/kitdef.js +1272 -0
  14. package/kits/kitadb/kitdef.js +606 -0
  15. package/kits/kitai/kitdef.js +2185 -0
  16. package/kits/kitansi/kitdef.js +1402 -0
  17. package/kits/kitcanvas/kitdef.js +914 -0
  18. package/kits/kitclippy/kitdef.js +925 -0
  19. package/kits/kitformat/kitdef.js +1485 -0
  20. package/kits/kitgps/kitdef.js +1862 -0
  21. package/kits/kitlibproc/kitdef.js +3 -2
  22. package/kits/kitmatrix/ex.js +19 -0
  23. package/kits/kitmatrix/kitdef.js +960 -0
  24. package/kits/kitmorse/kitdef.js +229 -0
  25. package/kits/kitmpatch/kitdef.js +906 -0
  26. package/kits/kitnet/kitdef.js +1401 -0
  27. package/kits/kitnovacweb/README.md +1416 -143
  28. package/kits/kitnovacweb/kitdef.js +92 -2
  29. package/kits/kitnovacweb/nvml/executor.js +578 -176
  30. package/kits/kitnovacweb/nvml/index.js +2 -2
  31. package/kits/kitnovacweb/nvml/lexer.js +72 -69
  32. package/kits/kitnovacweb/nvml/parser.js +328 -159
  33. package/kits/kitnovacweb/nvml/renderer.js +770 -270
  34. package/kits/kitparse/kitdef.js +1688 -0
  35. package/kits/kitproto/kitdef.js +613 -0
  36. package/kits/kitqr/kitdef.js +637 -0
  37. package/kits/kitregex++/kitdef.js +1353 -0
  38. package/kits/kitrequire/kitdef.js +1599 -0
  39. package/kits/kitx11/kitdef.js +1 -0
  40. package/kits/kitx11/kitx11.js +2472 -0
  41. package/kits/kitx11/kitx11_conn.js +948 -0
  42. package/kits/kitx11/kitx11_worker.js +121 -0
  43. package/kits/libtea/kitdef.js +2691 -0
  44. package/kits/libterm/ex.js +285 -0
  45. package/kits/libterm/kitdef.js +1927 -0
  46. package/novac/LICENSE +21 -0
  47. package/novac/README.md +1823 -0
  48. package/novac/bin/novac +950 -0
  49. package/novac/bin/nvc +522 -0
  50. package/novac/bin/nvml +542 -0
  51. package/novac/demo.nv +245 -0
  52. package/novac/demo_builtins.nv +209 -0
  53. package/novac/demo_http.nv +62 -0
  54. package/novac/examples/bf.nv +69 -0
  55. package/novac/examples/math.nv +21 -0
  56. package/novac/kits/kitai/kitdef.js +2185 -0
  57. package/novac/kits/kitansi/kitdef.js +1402 -0
  58. package/novac/kits/kitformat/kitdef.js +1485 -0
  59. package/novac/kits/kitgps/kitdef.js +1862 -0
  60. package/novac/kits/kitlibfs/kitdef.js +231 -0
  61. package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
  62. package/novac/kits/kitmatrix/ex.js +19 -0
  63. package/novac/kits/kitmatrix/kitdef.js +960 -0
  64. package/novac/kits/kitmpatch/kitdef.js +906 -0
  65. package/novac/kits/kitnovacweb/README.md +1572 -0
  66. package/novac/kits/kitnovacweb/demo.nv +12 -0
  67. package/novac/kits/kitnovacweb/demo.nvml +71 -0
  68. package/novac/kits/kitnovacweb/index.nova +12 -0
  69. package/novac/kits/kitnovacweb/kitdef.js +692 -0
  70. package/novac/kits/kitnovacweb/nova.kit.json +8 -0
  71. package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
  72. package/novac/kits/kitnovacweb/nvml/index.js +67 -0
  73. package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
  74. package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
  75. package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
  76. package/novac/kits/kitparse/kitdef.js +1688 -0
  77. package/novac/kits/kitregex++/kitdef.js +1353 -0
  78. package/novac/kits/kitrequire/kitdef.js +1599 -0
  79. package/novac/kits/kitx11/kitdef.js +1 -0
  80. package/novac/kits/kitx11/kitx11.js +2472 -0
  81. package/novac/kits/kitx11/kitx11_conn.js +948 -0
  82. package/novac/kits/kitx11/kitx11_worker.js +121 -0
  83. package/novac/kits/libtea/tf.js +2691 -0
  84. package/novac/kits/libterm/ex.js +285 -0
  85. package/novac/kits/libterm/kitdef.js +1927 -0
  86. package/novac/node_modules/chalk/license +9 -0
  87. package/novac/node_modules/chalk/package.json +83 -0
  88. package/novac/node_modules/chalk/readme.md +297 -0
  89. package/novac/node_modules/chalk/source/index.d.ts +325 -0
  90. package/novac/node_modules/chalk/source/index.js +225 -0
  91. package/novac/node_modules/chalk/source/utilities.js +33 -0
  92. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  93. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  94. package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  95. package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  96. package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  97. package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  98. package/novac/node_modules/commander/LICENSE +22 -0
  99. package/novac/node_modules/commander/Readme.md +1176 -0
  100. package/novac/node_modules/commander/esm.mjs +16 -0
  101. package/novac/node_modules/commander/index.js +24 -0
  102. package/novac/node_modules/commander/lib/argument.js +150 -0
  103. package/novac/node_modules/commander/lib/command.js +2777 -0
  104. package/novac/node_modules/commander/lib/error.js +39 -0
  105. package/novac/node_modules/commander/lib/help.js +747 -0
  106. package/novac/node_modules/commander/lib/option.js +380 -0
  107. package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
  108. package/novac/node_modules/commander/package-support.json +19 -0
  109. package/novac/node_modules/commander/package.json +82 -0
  110. package/novac/node_modules/commander/typings/esm.d.mts +3 -0
  111. package/novac/node_modules/commander/typings/index.d.ts +1113 -0
  112. package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
  113. package/novac/node_modules/node-addon-api/README.md +95 -0
  114. package/novac/node_modules/node-addon-api/common.gypi +21 -0
  115. package/novac/node_modules/node-addon-api/except.gypi +25 -0
  116. package/novac/node_modules/node-addon-api/index.js +14 -0
  117. package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
  118. package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
  119. package/novac/node_modules/node-addon-api/napi.h +3364 -0
  120. package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
  121. package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
  122. package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
  123. package/novac/node_modules/node-addon-api/package-support.json +21 -0
  124. package/novac/node_modules/node-addon-api/package.json +480 -0
  125. package/novac/node_modules/node-addon-api/tools/README.md +73 -0
  126. package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
  127. package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
  128. package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
  129. package/novac/node_modules/serialize-javascript/LICENSE +27 -0
  130. package/novac/node_modules/serialize-javascript/README.md +149 -0
  131. package/novac/node_modules/serialize-javascript/index.js +297 -0
  132. package/novac/node_modules/serialize-javascript/package.json +33 -0
  133. package/novac/package.json +27 -0
  134. package/novac/scripts/update-bin.js +24 -0
  135. package/novac/src/core/bstd.js +1035 -0
  136. package/novac/src/core/config.js +155 -0
  137. package/novac/src/core/describe.js +187 -0
  138. package/novac/src/core/emitter.js +499 -0
  139. package/novac/src/core/error.js +86 -0
  140. package/novac/src/core/executor.js +5606 -0
  141. package/novac/src/core/formatter.js +686 -0
  142. package/novac/src/core/lexer.js +1026 -0
  143. package/novac/src/core/nova_builtins.js +717 -0
  144. package/novac/src/core/nova_thread_worker.js +166 -0
  145. package/novac/src/core/parser.js +2181 -0
  146. package/novac/src/core/types.js +112 -0
  147. package/novac/src/index.js +28 -0
  148. package/novac/src/runtime/stdlib.js +244 -0
  149. package/package.json +6 -3
  150. package/scripts/update-bin.js +0 -0
  151. package/src/core/bstd.js +838 -362
  152. package/src/core/executor.js +2578 -170
  153. package/src/core/lexer.js +502 -54
  154. package/src/core/nova_builtins.js +21 -3
  155. package/src/core/parser.js +413 -72
  156. package/src/core/types.js +30 -2
  157. package/src/index.js +0 -0
  158. package/examples/example-project/README.md +0 -3
  159. package/examples/example-project/src/main.nova +0 -3
  160. package/src/core/environment.js +0 -0
  161. /package/{examples/example-project/bin/example-project.nv → novac/node_modules/node-addon-api/nothing.c} +0 -0
@@ -0,0 +1,1401 @@
1
+ // kitnet — novac networking kit
2
+ // Low-level networking primitives for novac.
3
+ // Covers: TCP, UDP, WebSocket, DNS, TLS, ICMP ping, port scanning,
4
+ // network interfaces, proxies, packet inspection, and more.
5
+ //
6
+ // Complements the core language's fetch(), URL types, and server{} block.
7
+
8
+ 'use strict';
9
+
10
+ const net = require('net');
11
+ const dgram = require('dgram');
12
+ const dns = require('dns');
13
+ const tls = require('tls');
14
+ const http = require('http');
15
+ const https = require('https');
16
+ const os = require('os');
17
+ const { EventEmitter } = require('events');
18
+ const { promisify } = require('util');
19
+
20
+ const dnsResolve4 = promisify(dns.resolve4);
21
+ const dnsResolve6 = promisify(dns.resolve6);
22
+ const dnsResolveMx = promisify(dns.resolveMx);
23
+ const dnsResolveTxt = promisify(dns.resolveTxt);
24
+ const dnsResolveSrv = promisify(dns.resolveSrv);
25
+ const dnsResolveNs = promisify(dns.resolveNs);
26
+ const dnsReverse = promisify(dns.reverse);
27
+ const dnsLookup = promisify(dns.lookup);
28
+
29
+ // ─── HELPERS ─────────────────────────────────────────────────────────────────
30
+
31
+ function defer() {
32
+ let resolve, reject;
33
+ const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
34
+ return { promise, resolve, reject };
35
+ }
36
+
37
+ function timeout(ms, msg = 'Timed out') {
38
+ return new Promise((_, reject) => setTimeout(() => reject(new Error(msg)), ms));
39
+ }
40
+
41
+ function race(promise, ms, msg) {
42
+ return Promise.race([promise, timeout(ms, msg)]);
43
+ }
44
+
45
+ // ─── TCP ─────────────────────────────────────────────────────────────────────
46
+
47
+ const tcp = {
48
+ /**
49
+ * Open a TCP connection to host:port.
50
+ * Returns a socket wrapper with send/receive/close.
51
+ * @param {string} host
52
+ * @param {number} port
53
+ * @param {object} [opts]
54
+ * @param {number} [opts.timeout=10000]
55
+ * @param {string} [opts.encoding='utf8']
56
+ * @returns {Promise<TcpSocket>}
57
+ */
58
+ async connect(host, port, opts = {}) {
59
+ const enc = opts.encoding ?? 'utf8';
60
+ const d = defer();
61
+ const socket = net.createConnection({ host, port }, () => d.resolve(socket));
62
+ socket.setTimeout(opts.timeout ?? 10000);
63
+ socket.on('error', d.reject);
64
+ await race(d.promise, opts.timeout ?? 10000, `TCP connect to ${host}:${port} timed out`);
65
+
66
+ const emitter = new EventEmitter();
67
+ socket.setEncoding(enc);
68
+ socket.on('data', data => emitter.emit('data', data));
69
+ socket.on('close', () => emitter.emit('close'));
70
+ socket.on('error', err => emitter.emit('error', err));
71
+ socket.on('timeout', () => { emitter.emit('timeout'); socket.destroy(); });
72
+
73
+ return {
74
+ host, port,
75
+ /** Send data over the socket. */
76
+ send(data) {
77
+ return new Promise((res, rej) => socket.write(data, err => err ? rej(err) : res()));
78
+ },
79
+ /** Receive the next chunk of data. */
80
+ receive(timeoutMs = 10000) {
81
+ return race(new Promise(res => emitter.once('data', res)), timeoutMs, 'Receive timed out');
82
+ },
83
+ /** Receive all data until the socket closes. */
84
+ receiveAll(timeoutMs = 30000) {
85
+ return race(new Promise(res => {
86
+ const chunks = [];
87
+ emitter.on('data', d => chunks.push(d));
88
+ emitter.once('close', () => res(chunks.join('')));
89
+ }), timeoutMs, 'receiveAll timed out');
90
+ },
91
+ /** Pipe received data to a callback. */
92
+ onData(fn) { emitter.on('data', fn); return this; },
93
+ onClose(fn) { emitter.on('close', fn); return this; },
94
+ onError(fn) { emitter.on('error', fn); return this; },
95
+ /** Close the connection. */
96
+ close() { socket.destroy(); },
97
+ /** Underlying net.Socket for advanced use. */
98
+ raw: socket,
99
+ };
100
+ },
101
+
102
+ /**
103
+ * Create a TCP server.
104
+ * @param {number} port
105
+ * @param {string} [host='0.0.0.0']
106
+ * @param {function} handler - Called with each TcpSocket connection.
107
+ * @returns {Promise<TcpServer>}
108
+ */
109
+ async serve(port, host = '0.0.0.0', handler) {
110
+ if (typeof host === 'function') { handler = host; host = '0.0.0.0'; }
111
+ const server = net.createServer(socket => {
112
+ const emitter = new EventEmitter();
113
+ socket.setEncoding('utf8');
114
+ socket.on('data', d => emitter.emit('data', d));
115
+ socket.on('close', () => emitter.emit('close'));
116
+ socket.on('error', e => emitter.emit('error', e));
117
+
118
+ const conn = {
119
+ remoteAddress: socket.remoteAddress,
120
+ remotePort: socket.remotePort,
121
+ send(data) { return new Promise((res, rej) => socket.write(data, e => e ? rej(e) : res())); },
122
+ receive(ms = 10000) { return race(new Promise(res => emitter.once('data', res)), ms, 'Receive timed out'); },
123
+ receiveAll(ms = 30000) {
124
+ return race(new Promise(res => {
125
+ const chunks = [];
126
+ emitter.on('data', d => chunks.push(d));
127
+ emitter.once('close', () => res(chunks.join('')));
128
+ }), ms, 'receiveAll timed out');
129
+ },
130
+ onData(fn) { emitter.on('data', fn); return this; },
131
+ onClose(fn) { emitter.on('close', fn); return this; },
132
+ onError(fn) { emitter.on('error', fn); return this; },
133
+ close() { socket.destroy(); },
134
+ raw: socket,
135
+ };
136
+ handler(conn);
137
+ });
138
+
139
+ await new Promise((res, rej) => server.listen(port, host, res).on('error', rej));
140
+
141
+ return {
142
+ port, host,
143
+ close() { return new Promise(res => server.close(res)); },
144
+ raw: server,
145
+ };
146
+ },
147
+
148
+ /**
149
+ * Check if a TCP port is open on a host.
150
+ * @param {string} host
151
+ * @param {number} port
152
+ * @param {number} [timeoutMs=3000]
153
+ * @returns {Promise<boolean>}
154
+ */
155
+ async isOpen(host, port, timeoutMs = 3000) {
156
+ return new Promise(resolve => {
157
+ const socket = net.createConnection({ host, port });
158
+ socket.setTimeout(timeoutMs);
159
+ socket.on('connect', () => { socket.destroy(); resolve(true); });
160
+ socket.on('error', () => resolve(false));
161
+ socket.on('timeout', () => { socket.destroy(); resolve(false); });
162
+ });
163
+ },
164
+
165
+ /**
166
+ * Send data and get response in a single call (connect → send → receive → close).
167
+ * @param {string} host
168
+ * @param {number} port
169
+ * @param {string|Buffer} data
170
+ * @param {object} [opts]
171
+ * @returns {Promise<string>}
172
+ */
173
+ async request(host, port, data, opts = {}) {
174
+ const socket = await tcp.connect(host, port, opts);
175
+ await socket.send(data);
176
+ const response = await socket.receiveAll(opts.timeout ?? 10000);
177
+ socket.close();
178
+ return response;
179
+ },
180
+ };
181
+
182
+ // ─── UDP ─────────────────────────────────────────────────────────────────────
183
+
184
+ const udp = {
185
+ /**
186
+ * Send a UDP datagram.
187
+ * @param {string} host
188
+ * @param {number} port
189
+ * @param {string|Buffer} data
190
+ * @param {'udp4'|'udp6'} [type='udp4']
191
+ * @returns {Promise<void>}
192
+ */
193
+ send(host, port, data, type = 'udp4') {
194
+ return new Promise((resolve, reject) => {
195
+ const client = dgram.createSocket(type);
196
+ const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
197
+ client.send(buf, port, host, err => {
198
+ client.close();
199
+ err ? reject(err) : resolve();
200
+ });
201
+ });
202
+ },
203
+
204
+ /**
205
+ * Send a UDP datagram and wait for a response.
206
+ * @param {string} host
207
+ * @param {number} port
208
+ * @param {string|Buffer} data
209
+ * @param {object} [opts]
210
+ * @returns {Promise<{ data: Buffer, rinfo: object }>}
211
+ */
212
+ request(host, port, data, opts = {}) {
213
+ const timeoutMs = opts.timeout ?? 5000;
214
+ const type = opts.type ?? 'udp4';
215
+ return race(new Promise((resolve, reject) => {
216
+ const client = dgram.createSocket(type);
217
+ const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
218
+ client.send(buf, port, host, err => { if (err) { client.close(); reject(err); } });
219
+ client.on('message', (msg, rinfo) => { client.close(); resolve({ data: msg, rinfo }); });
220
+ client.on('error', err => { client.close(); reject(err); });
221
+ }), timeoutMs, `UDP request to ${host}:${port} timed out`);
222
+ },
223
+
224
+ /**
225
+ * Create a UDP socket that listens for incoming datagrams.
226
+ * @param {number} port
227
+ * @param {string} [host='0.0.0.0']
228
+ * @param {'udp4'|'udp6'} [type='udp4']
229
+ * @returns {Promise<UdpServer>}
230
+ */
231
+ async listen(port, host = '0.0.0.0', type = 'udp4') {
232
+ const socket = dgram.createSocket(type);
233
+ const emitter = new EventEmitter();
234
+ socket.on('message', (msg, rinfo) => emitter.emit('message', { data: msg, rinfo }));
235
+ socket.on('error', err => emitter.emit('error', err));
236
+
237
+ await new Promise((res, rej) => socket.bind(port, host, res).on('error', rej));
238
+
239
+ return {
240
+ port, host,
241
+ onMessage(fn) { emitter.on('message', fn); return this; },
242
+ onError(fn) { emitter.on('error', fn); return this; },
243
+ send(data, toHost, toPort) {
244
+ const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
245
+ return new Promise((res, rej) => socket.send(buf, toPort, toHost, e => e ? rej(e) : res()));
246
+ },
247
+ close() { return new Promise(res => socket.close(res)); },
248
+ raw: socket,
249
+ };
250
+ },
251
+
252
+ /**
253
+ * Join a multicast group.
254
+ * @param {string} multicastAddr
255
+ * @param {number} port
256
+ * @param {string} [iface]
257
+ * @returns {Promise<UdpServer>}
258
+ */
259
+ async multicast(multicastAddr, port, iface) {
260
+ const server = await udp.listen(port);
261
+ server.raw.addMembership(multicastAddr, iface);
262
+ return server;
263
+ },
264
+ };
265
+
266
+ // ─── TLS ─────────────────────────────────────────────────────────────────────
267
+
268
+ const tlsKit = {
269
+ /**
270
+ * Open a TLS connection.
271
+ * @param {string} host
272
+ * @param {number} port
273
+ * @param {object} [opts] - Passed to tls.connect (ca, cert, key, rejectUnauthorized, etc.)
274
+ * @returns {Promise<TlsSocket>}
275
+ */
276
+ async connect(host, port, opts = {}) {
277
+ const d = defer();
278
+ const socket = tls.connect({ host, port, ...opts }, () => d.resolve(socket));
279
+ socket.on('error', d.reject);
280
+ await race(d.promise, opts.timeout ?? 10000, `TLS connect to ${host}:${port} timed out`);
281
+
282
+ const emitter = new EventEmitter();
283
+ socket.setEncoding(opts.encoding ?? 'utf8');
284
+ socket.on('data', d => emitter.emit('data', d));
285
+ socket.on('close', () => emitter.emit('close'));
286
+ socket.on('error', e => emitter.emit('error', e));
287
+
288
+ return {
289
+ host, port,
290
+ authorized: socket.authorized,
291
+ authError: socket.authorizationError,
292
+ certificate: socket.getPeerCertificate(),
293
+ cipher: socket.getCipher(),
294
+ protocol: socket.getProtocol(),
295
+ send(data) { return new Promise((res, rej) => socket.write(data, e => e ? rej(e) : res())); },
296
+ receive(ms = 10000) { return race(new Promise(res => emitter.once('data', res)), ms, 'Receive timed out'); },
297
+ receiveAll(ms = 30000) {
298
+ return race(new Promise(res => {
299
+ const chunks = [];
300
+ emitter.on('data', d => chunks.push(d));
301
+ emitter.once('close', () => res(chunks.join('')));
302
+ }), ms, 'receiveAll timed out');
303
+ },
304
+ onData(fn) { emitter.on('data', fn); return this; },
305
+ onClose(fn) { emitter.on('close', fn); return this; },
306
+ onError(fn) { emitter.on('error', fn); return this; },
307
+ close() { socket.destroy(); },
308
+ raw: socket,
309
+ };
310
+ },
311
+
312
+ /**
313
+ * Create a TLS server.
314
+ * @param {number} port
315
+ * @param {object} opts - Must include cert and key (PEM strings or Buffers).
316
+ * @param {function} handler
317
+ * @returns {Promise<TlsServer>}
318
+ */
319
+ async serve(port, opts, handler) {
320
+ const server = tls.createServer(opts, socket => {
321
+ const emitter = new EventEmitter();
322
+ socket.setEncoding('utf8');
323
+ socket.on('data', d => emitter.emit('data', d));
324
+ socket.on('close', () => emitter.emit('close'));
325
+ socket.on('error', e => emitter.emit('error', e));
326
+
327
+ handler({
328
+ remoteAddress: socket.remoteAddress,
329
+ remotePort: socket.remotePort,
330
+ authorized: socket.authorized,
331
+ certificate: socket.getPeerCertificate(),
332
+ send(data) { return new Promise((res, rej) => socket.write(data, e => e ? rej(e) : res())); },
333
+ receive(ms = 10000) { return race(new Promise(res => emitter.once('data', res)), ms, 'Receive timed out'); },
334
+ onData(fn) { emitter.on('data', fn); return this; },
335
+ onClose(fn) { emitter.on('close', fn); return this; },
336
+ onError(fn) { emitter.on('error', fn); return this; },
337
+ close() { socket.destroy(); },
338
+ raw: socket,
339
+ });
340
+ });
341
+
342
+ await new Promise((res, rej) => server.listen(port, res).on('error', rej));
343
+ return {
344
+ port,
345
+ close() { return new Promise(res => server.close(res)); },
346
+ raw: server,
347
+ };
348
+ },
349
+
350
+ /**
351
+ * Get TLS certificate info for a host.
352
+ * @param {string} host
353
+ * @param {number} [port=443]
354
+ * @returns {Promise<object>}
355
+ */
356
+ async getCert(host, port = 443) {
357
+ const socket = await tlsKit.connect(host, port, { rejectUnauthorized: false });
358
+ const cert = socket.certificate;
359
+ socket.close();
360
+ return cert;
361
+ },
362
+
363
+ /**
364
+ * Check if a host's TLS certificate is valid and not expired.
365
+ * @param {string} host
366
+ * @param {number} [port=443]
367
+ * @returns {Promise<{ valid: boolean, expires: Date, daysLeft: number, issuer: object }>}
368
+ */
369
+ async checkCert(host, port = 443) {
370
+ const cert = await tlsKit.getCert(host, port);
371
+ const expires = new Date(cert.valid_to);
372
+ const now = new Date();
373
+ const daysLeft = Math.floor((expires - now) / 86400000);
374
+ return {
375
+ valid: daysLeft > 0 && cert.subject !== undefined,
376
+ expires,
377
+ daysLeft,
378
+ issuer: cert.issuer,
379
+ subject: cert.subject,
380
+ fingerprint: cert.fingerprint,
381
+ };
382
+ },
383
+ };
384
+
385
+ // ─── WEBSOCKET ────────────────────────────────────────────────────────────────
386
+ // Pure implementation — no external deps.
387
+
388
+ const ws = {
389
+ /**
390
+ * Connect to a WebSocket server.
391
+ * @param {string} url - ws:// or wss://
392
+ * @param {object} [opts]
393
+ * @returns {Promise<WsSocket>}
394
+ */
395
+ async connect(url, opts = {}) {
396
+ const parsed = new URL(url);
397
+ const isSecure = parsed.protocol === 'wss:';
398
+ const port = parseInt(parsed.port) || (isSecure ? 443 : 80);
399
+ const host = parsed.hostname;
400
+ const path = parsed.pathname + (parsed.search || '');
401
+
402
+ // WebSocket handshake key
403
+ const key = Buffer.from(Math.random().toString(36).repeat(3)).toString('base64').slice(0, 24);
404
+ const headers = [
405
+ `GET ${path} HTTP/1.1`,
406
+ `Host: ${host}`,
407
+ 'Upgrade: websocket',
408
+ 'Connection: Upgrade',
409
+ `Sec-WebSocket-Key: ${key}`,
410
+ 'Sec-WebSocket-Version: 13',
411
+ ...(opts.headers ? Object.entries(opts.headers).map(([k,v]) => `${k}: ${v}`) : []),
412
+ '', '',
413
+ ].join('\r\n');
414
+
415
+ const rawSocket = isSecure
416
+ ? await tlsKit.connect(host, port, { rejectUnauthorized: opts.rejectUnauthorized ?? true })
417
+ : await tcp.connect(host, port, opts);
418
+
419
+ // Send handshake
420
+ await rawSocket.send(headers);
421
+
422
+ // Read handshake response
423
+ const response = await rawSocket.receive(opts.timeout ?? 5000);
424
+ if (!response.includes('101')) throw new Error(`WebSocket upgrade failed: ${response.split('\r\n')[0]}`);
425
+
426
+ const emitter = new EventEmitter();
427
+ let buffer = Buffer.alloc(0);
428
+
429
+ // WebSocket frame parser
430
+ rawSocket.raw.removeAllListeners('data');
431
+ rawSocket.raw.on('data', chunk => {
432
+ buffer = Buffer.concat([buffer, chunk]);
433
+ while (buffer.length >= 2) {
434
+ const fin = (buffer[0] & 0x80) !== 0;
435
+ const opcode = buffer[0] & 0x0f;
436
+ let payloadLen = buffer[1] & 0x7f;
437
+ let offset = 2;
438
+
439
+ if (payloadLen === 126) { if (buffer.length < 4) break; payloadLen = buffer.readUInt16BE(2); offset = 4; }
440
+ else if (payloadLen === 127) { if (buffer.length < 10) break; payloadLen = Number(buffer.readBigUInt64BE(2)); offset = 10; }
441
+
442
+ if (buffer.length < offset + payloadLen) break;
443
+
444
+ const payload = buffer.slice(offset, offset + payloadLen);
445
+ buffer = buffer.slice(offset + payloadLen);
446
+
447
+ if (opcode === 0x1) emitter.emit('message', payload.toString('utf8'));
448
+ else if (opcode === 0x2) emitter.emit('message', payload);
449
+ else if (opcode === 0x8) emitter.emit('close');
450
+ else if (opcode === 0x9) {
451
+ // ping — send pong
452
+ const pong = Buffer.from([0x8a, 0x00]);
453
+ rawSocket.raw.write(pong);
454
+ }
455
+ }
456
+ });
457
+
458
+ rawSocket.raw.on('close', () => emitter.emit('close'));
459
+ rawSocket.raw.on('error', e => emitter.emit('error', e));
460
+
461
+ // WebSocket frame encoder (client, so mask=true)
462
+ function encode(data, opcode = 0x1) {
463
+ const payload = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
464
+ const len = payload.length;
465
+ const mask = Buffer.from([
466
+ Math.random() * 256 | 0, Math.random() * 256 | 0,
467
+ Math.random() * 256 | 0, Math.random() * 256 | 0,
468
+ ]);
469
+ const head = len < 126
470
+ ? Buffer.from([0x80 | opcode, 0x80 | len])
471
+ : len < 65536
472
+ ? Buffer.from([0x80 | opcode, 0xfe, len >> 8, len & 0xff])
473
+ : (() => { const b = Buffer.alloc(10); b[0] = 0x80 | opcode; b[1] = 0xff; b.writeBigUInt64BE(BigInt(len), 2); return b; })();
474
+ const masked = Buffer.allocUnsafe(len);
475
+ for (let i = 0; i < len; i++) masked[i] = payload[i] ^ mask[i % 4];
476
+ return Buffer.concat([head, mask, masked]);
477
+ }
478
+
479
+ return {
480
+ url,
481
+ send(data) {
482
+ const opcode = Buffer.isBuffer(data) ? 0x2 : 0x1;
483
+ return new Promise((res, rej) => rawSocket.raw.write(encode(data, opcode), e => e ? rej(e) : res()));
484
+ },
485
+ sendBinary(data) { return this.send(Buffer.isBuffer(data) ? data : Buffer.from(data)); },
486
+ receive(ms = 30000) { return race(new Promise(res => emitter.once('message', res)), ms, 'WS receive timed out'); },
487
+ onMessage(fn) { emitter.on('message', fn); return this; },
488
+ onClose(fn) { emitter.on('close', fn); return this; },
489
+ onError(fn) { emitter.on('error', fn); return this; },
490
+ close() {
491
+ rawSocket.raw.write(Buffer.from([0x88, 0x80, 0, 0, 0, 0]));
492
+ rawSocket.close();
493
+ },
494
+ raw: rawSocket.raw,
495
+ };
496
+ },
497
+
498
+ /**
499
+ * Create a WebSocket server (upgrade from HTTP).
500
+ * @param {number} port
501
+ * @param {function} handler - Called with each WsSocket.
502
+ * @param {object} [opts]
503
+ * @returns {Promise<WsServer>}
504
+ */
505
+ async serve(port, handler, opts = {}) {
506
+ const crypto = require('crypto');
507
+ const MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
508
+
509
+ const server = http.createServer((req, res) => {
510
+ if (!req.headers['upgrade']) { res.writeHead(426); res.end('Upgrade required'); return; }
511
+ });
512
+
513
+ server.on('upgrade', (req, socket) => {
514
+ const key = req.headers['sec-websocket-key'];
515
+ const accept = crypto.createHash('sha1').update(key + MAGIC).digest('base64');
516
+
517
+ socket.write([
518
+ 'HTTP/1.1 101 Switching Protocols',
519
+ 'Upgrade: websocket',
520
+ 'Connection: Upgrade',
521
+ `Sec-WebSocket-Accept: ${accept}`,
522
+ '', '',
523
+ ].join('\r\n'));
524
+
525
+ const emitter = new EventEmitter();
526
+ let buffer = Buffer.alloc(0);
527
+
528
+ socket.on('data', chunk => {
529
+ buffer = Buffer.concat([buffer, chunk]);
530
+ while (buffer.length >= 2) {
531
+ const opcode = buffer[0] & 0x0f;
532
+ const masked = (buffer[1] & 0x80) !== 0;
533
+ let payloadLen = buffer[1] & 0x7f;
534
+ let offset = 2;
535
+
536
+ if (payloadLen === 126) { if (buffer.length < 4) break; payloadLen = buffer.readUInt16BE(2); offset = 4; }
537
+ else if (payloadLen === 127) { if (buffer.length < 10) break; payloadLen = Number(buffer.readBigUInt64BE(2)); offset = 10; }
538
+
539
+ const maskLen = masked ? 4 : 0;
540
+ if (buffer.length < offset + maskLen + payloadLen) break;
541
+
542
+ const maskKey = masked ? buffer.slice(offset, offset + 4) : null;
543
+ offset += maskLen;
544
+ const raw = buffer.slice(offset, offset + payloadLen);
545
+ buffer = buffer.slice(offset + payloadLen);
546
+
547
+ const payload = masked ? Buffer.from(raw.map((b, i) => b ^ maskKey[i % 4])) : raw;
548
+ if (opcode === 0x1) emitter.emit('message', payload.toString('utf8'));
549
+ else if (opcode === 0x2) emitter.emit('message', payload);
550
+ else if (opcode === 0x8) { emitter.emit('close'); socket.destroy(); }
551
+ else if (opcode === 0x9) socket.write(Buffer.from([0x8a, 0x00]));
552
+ }
553
+ });
554
+
555
+ socket.on('close', () => emitter.emit('close'));
556
+ socket.on('error', e => emitter.emit('error', e));
557
+
558
+ function encodeServer(data) {
559
+ const payload = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
560
+ const len = payload.length;
561
+ const opcode = Buffer.isBuffer(data) ? 0x2 : 0x1;
562
+ if (len < 126) return Buffer.concat([Buffer.from([0x80 | opcode, len]), payload]);
563
+ if (len < 65536) return Buffer.concat([Buffer.from([0x80 | opcode, 126, len >> 8, len & 0xff]), payload]);
564
+ const head = Buffer.alloc(10); head[0] = 0x80 | opcode; head[1] = 127; head.writeBigUInt64BE(BigInt(len), 2);
565
+ return Buffer.concat([head, payload]);
566
+ }
567
+
568
+ handler({
569
+ remoteAddress: socket.remoteAddress,
570
+ remotePort: socket.remotePort,
571
+ headers: req.headers,
572
+ url: req.url,
573
+ send(data) { return new Promise((res, rej) => socket.write(encodeServer(data), e => e ? rej(e) : res())); },
574
+ sendBinary(data){ return this.send(Buffer.isBuffer(data) ? data : Buffer.from(data)); },
575
+ receive(ms = 30000) { return race(new Promise(res => emitter.once('message', res)), ms, 'WS receive timed out'); },
576
+ onMessage(fn) { emitter.on('message', fn); return this; },
577
+ onClose(fn) { emitter.on('close', fn); return this; },
578
+ onError(fn) { emitter.on('error', fn); return this; },
579
+ close() { socket.write(Buffer.from([0x88, 0x00])); socket.destroy(); },
580
+ raw: socket,
581
+ });
582
+ });
583
+
584
+ await new Promise((res, rej) => server.listen(port, res).on('error', rej));
585
+ return {
586
+ port,
587
+ close() { return new Promise(res => server.close(res)); },
588
+ raw: server,
589
+ };
590
+ },
591
+ };
592
+
593
+ // ─── DNS ─────────────────────────────────────────────────────────────────────
594
+
595
+ const dnsKit = {
596
+ /** Resolve a hostname to IPv4 addresses. */
597
+ async resolve4(hostname) { return dnsResolve4(hostname); },
598
+ /** Resolve a hostname to IPv6 addresses. */
599
+ async resolve6(hostname) { return dnsResolve6(hostname); },
600
+ /** Resolve MX records. */
601
+ async mx(hostname) { return dnsResolveMx(hostname); },
602
+ /** Resolve TXT records. */
603
+ async txt(hostname) { return dnsResolveTxt(hostname); },
604
+ /** Resolve SRV records. */
605
+ async srv(hostname) { return dnsResolveSrv(hostname); },
606
+ /** Resolve NS records. */
607
+ async ns(hostname) { return dnsResolveNs(hostname); },
608
+ /** Reverse lookup — IP → hostname. */
609
+ async reverse(ip) { return dnsReverse(ip); },
610
+ /**
611
+ * Full lookup — returns address + family.
612
+ * @param {string} hostname
613
+ * @param {4|6} [family]
614
+ */
615
+ async lookup(hostname, family) {
616
+ return dnsLookup(hostname, family ? { family } : {});
617
+ },
618
+ /**
619
+ * Resolve all record types for a hostname.
620
+ * @param {string} hostname
621
+ * @returns {Promise<object>}
622
+ */
623
+ async all(hostname) {
624
+ const results = {};
625
+ const safe = async (fn) => { try { return await fn(); } catch { return null; } };
626
+ [results.a, results.aaaa, results.mx, results.txt, results.ns, results.srv] = await Promise.all([
627
+ safe(() => dnsResolve4(hostname)),
628
+ safe(() => dnsResolve6(hostname)),
629
+ safe(() => dnsResolveMx(hostname)),
630
+ safe(() => dnsResolveTxt(hostname)),
631
+ safe(() => dnsResolveNs(hostname)),
632
+ safe(() => dnsResolveSrv(hostname)),
633
+ ]);
634
+ return results;
635
+ },
636
+ /** Change the DNS servers used for resolution. */
637
+ setServers(servers) { dns.setServers(servers); },
638
+ /** Get the currently configured DNS servers. */
639
+ getServers() { return dns.getServers(); },
640
+ };
641
+
642
+ // ─── PING ─────────────────────────────────────────────────────────────────────
643
+ // Uses TCP echo (port 7) or HTTP HEAD as a proxy for ICMP ping.
644
+ // Real ICMP requires raw sockets (root/admin), so we use TCP reachability.
645
+
646
+ const ping = {
647
+ /**
648
+ * Ping a host via TCP connection attempt.
649
+ * @param {string} host
650
+ * @param {object} [opts]
651
+ * @param {number} [opts.port=80]
652
+ * @param {number} [opts.count=4]
653
+ * @param {number} [opts.timeout=3000]
654
+ * @returns {Promise<PingResult>}
655
+ */
656
+ async tcp(host, opts = {}) {
657
+ const port = opts.port ?? 80;
658
+ const count = opts.count ?? 4;
659
+ const timeoutMs = opts.timeout ?? 3000;
660
+ const results = [];
661
+
662
+ for (let i = 0; i < count; i++) {
663
+ const start = Date.now();
664
+ const reachable = await tcp.isOpen(host, port, timeoutMs);
665
+ const rtt = Date.now() - start;
666
+ results.push({ seq: i + 1, rtt: reachable ? rtt : null, success: reachable });
667
+ }
668
+
669
+ const successful = results.filter(r => r.success);
670
+ const rtts = successful.map(r => r.rtt);
671
+
672
+ return {
673
+ host, port,
674
+ sent: count,
675
+ received: successful.length,
676
+ lost: count - successful.length,
677
+ packetLoss: `${((count - successful.length) / count * 100).toFixed(1)}%`,
678
+ rtt: {
679
+ min: rtts.length ? Math.min(...rtts) : null,
680
+ max: rtts.length ? Math.max(...rtts) : null,
681
+ avg: rtts.length ? Math.round(rtts.reduce((a, b) => a + b, 0) / rtts.length) : null,
682
+ },
683
+ results,
684
+ };
685
+ },
686
+
687
+ /**
688
+ * Ping a host via HTTP HEAD request.
689
+ * @param {string} url
690
+ * @param {object} [opts]
691
+ * @returns {Promise<HttpPingResult>}
692
+ */
693
+ async http(url, opts = {}) {
694
+ const count = opts.count ?? 4;
695
+ const timeoutMs = opts.timeout ?? 5000;
696
+ const results = [];
697
+
698
+ for (let i = 0; i < count; i++) {
699
+ const start = Date.now();
700
+ try {
701
+ await httpKit.head(url, { timeout: timeoutMs });
702
+ results.push({ seq: i + 1, rtt: Date.now() - start, success: true });
703
+ } catch {
704
+ results.push({ seq: i + 1, rtt: null, success: false });
705
+ }
706
+ }
707
+
708
+ const successful = results.filter(r => r.success);
709
+ const rtts = successful.map(r => r.rtt);
710
+ return {
711
+ url,
712
+ sent: count, received: successful.length,
713
+ lost: count - successful.length,
714
+ packetLoss: `${((count - successful.length) / count * 100).toFixed(1)}%`,
715
+ rtt: {
716
+ min: rtts.length ? Math.min(...rtts) : null,
717
+ max: rtts.length ? Math.max(...rtts) : null,
718
+ avg: rtts.length ? Math.round(rtts.reduce((a, b) => a + b, 0) / rtts.length) : null,
719
+ },
720
+ results,
721
+ };
722
+ },
723
+ };
724
+
725
+ // ─── PORT SCANNER ─────────────────────────────────────────────────────────────
726
+
727
+ const portScan = {
728
+ /**
729
+ * Scan a single port.
730
+ * @param {string} host
731
+ * @param {number} port
732
+ * @param {number} [timeoutMs=1000]
733
+ * @returns {Promise<{ port, open, banner }>}
734
+ */
735
+ async one(host, port, timeoutMs = 1000) {
736
+ const open = await tcp.isOpen(host, port, timeoutMs);
737
+ let banner = null;
738
+ if (open) {
739
+ try {
740
+ const socket = await tcp.connect(host, port, { timeout: timeoutMs });
741
+ banner = await Promise.race([socket.receive(500), timeout(500).catch(() => null)]).catch(() => null);
742
+ socket.close();
743
+ } catch {}
744
+ }
745
+ return { port, open, banner };
746
+ },
747
+
748
+ /**
749
+ * Scan a range of ports.
750
+ * @param {string} host
751
+ * @param {number} start
752
+ * @param {number} end
753
+ * @param {object} [opts]
754
+ * @param {number} [opts.concurrency=50]
755
+ * @param {number} [opts.timeout=1000]
756
+ * @returns {Promise<ScanResult[]>}
757
+ */
758
+ async range(host, start, end, opts = {}) {
759
+ const concurrency = opts.concurrency ?? 50;
760
+ const timeoutMs = opts.timeout ?? 1000;
761
+ const ports = Array.from({ length: end - start + 1 }, (_, i) => start + i);
762
+ const results = [];
763
+
764
+ for (let i = 0; i < ports.length; i += concurrency) {
765
+ const batch = ports.slice(i, i + concurrency);
766
+ const chunk = await Promise.all(batch.map(p => portScan.one(host, p, timeoutMs)));
767
+ results.push(...chunk);
768
+ }
769
+
770
+ return results;
771
+ },
772
+
773
+ /**
774
+ * Scan a list of specific ports.
775
+ * @param {string} host
776
+ * @param {number[]} ports
777
+ * @param {object} [opts]
778
+ * @returns {Promise<ScanResult[]>}
779
+ */
780
+ async list(host, ports, opts = {}) {
781
+ const concurrency = opts.concurrency ?? 50;
782
+ const timeoutMs = opts.timeout ?? 1000;
783
+ const results = [];
784
+
785
+ for (let i = 0; i < ports.length; i += concurrency) {
786
+ const batch = ports.slice(i, i + concurrency);
787
+ const chunk = await Promise.all(batch.map(p => portScan.one(host, p, timeoutMs)));
788
+ results.push(...chunk);
789
+ }
790
+
791
+ return results;
792
+ },
793
+
794
+ /**
795
+ * Scan common well-known ports.
796
+ * @param {string} host
797
+ * @param {object} [opts]
798
+ * @returns {Promise<ScanResult[]>}
799
+ */
800
+ common(host, opts = {}) {
801
+ return portScan.list(host, [
802
+ 21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445,
803
+ 993, 995, 1723, 3306, 3389, 5900, 8080, 8443, 8888,
804
+ ], opts);
805
+ },
806
+ };
807
+
808
+ // ─── HTTP(S) UTILITIES ────────────────────────────────────────────────────────
809
+
810
+ const httpKit = {
811
+ /**
812
+ * Low-level HTTP/HTTPS request.
813
+ * @param {string} method
814
+ * @param {string} url
815
+ * @param {object} [opts]
816
+ * @returns {Promise<HttpResponse>}
817
+ */
818
+ request(method, url, opts = {}) {
819
+ return new Promise((resolve, reject) => {
820
+ const parsed = new URL(url);
821
+ const isHttps = parsed.protocol === 'https:';
822
+ const lib = isHttps ? https : http;
823
+ const port = parseInt(parsed.port) || (isHttps ? 443 : 80);
824
+
825
+ const reqOpts = {
826
+ hostname: parsed.hostname,
827
+ port,
828
+ path: parsed.pathname + parsed.search,
829
+ method: method.toUpperCase(),
830
+ headers: opts.headers ?? {},
831
+ timeout: opts.timeout ?? 30000,
832
+ };
833
+
834
+ if (opts.body && !reqOpts.headers['Content-Length']) {
835
+ const body = typeof opts.body === 'string' ? opts.body : JSON.stringify(opts.body);
836
+ reqOpts.headers['Content-Length'] = Buffer.byteLength(body);
837
+ if (!reqOpts.headers['Content-Type']) reqOpts.headers['Content-Type'] = 'application/json';
838
+ }
839
+
840
+ const req = lib.request(reqOpts, res => {
841
+ const chunks = [];
842
+ res.on('data', d => chunks.push(d));
843
+ res.on('end', () => {
844
+ const raw = Buffer.concat(chunks);
845
+ const text = raw.toString('utf8');
846
+ let json = null;
847
+ try { json = JSON.parse(text); } catch {}
848
+ resolve({
849
+ status: res.statusCode,
850
+ statusText: res.statusMessage,
851
+ headers: res.headers,
852
+ text,
853
+ json,
854
+ ok: res.statusCode >= 200 && res.statusCode < 300,
855
+ raw,
856
+ });
857
+ });
858
+ res.on('error', reject);
859
+ });
860
+
861
+ req.setTimeout(opts.timeout ?? 30000, () => { req.destroy(); reject(new Error('HTTP request timed out')); });
862
+ req.on('error', reject);
863
+
864
+ if (opts.body) {
865
+ const body = typeof opts.body === 'string' ? opts.body : JSON.stringify(opts.body);
866
+ req.write(body);
867
+ }
868
+ req.end();
869
+ });
870
+ },
871
+
872
+ get(url, opts = {}) { return httpKit.request('GET', url, opts); },
873
+ post(url, body, opts = {}) { return httpKit.request('POST', url, { ...opts, body }); },
874
+ put(url, body, opts = {}) { return httpKit.request('PUT', url, { ...opts, body }); },
875
+ patch(url, body, opts = {}) { return httpKit.request('PATCH', url, { ...opts, body }); },
876
+ delete(url, opts = {}) { return httpKit.request('DELETE', url, opts); },
877
+ head(url, opts = {}) { return httpKit.request('HEAD', url, opts); },
878
+ options(url, opts = {}) { return httpKit.request('OPTIONS', url, opts); },
879
+
880
+ /**
881
+ * Follow redirects manually and return the redirect chain.
882
+ * @param {string} url
883
+ * @param {number} [maxRedirects=10]
884
+ * @returns {Promise<{ chain: string[], final: string, response: HttpResponse }>}
885
+ */
886
+ async traceRedirects(url, maxRedirects = 10) {
887
+ const chain = [url];
888
+ let current = url;
889
+ for (let i = 0; i < maxRedirects; i++) {
890
+ const res = await httpKit.request('HEAD', current, { timeout: 5000 });
891
+ if (res.status < 300 || res.status >= 400) break;
892
+ const next = res.headers['location'];
893
+ if (!next) break;
894
+ current = next.startsWith('http') ? next : new URL(next, current).href;
895
+ chain.push(current);
896
+ }
897
+ const response = await httpKit.get(current);
898
+ return { chain, final: current, response };
899
+ },
900
+
901
+ /**
902
+ * Download a file from a URL.
903
+ * @param {string} url
904
+ * @param {string} destPath
905
+ * @returns {Promise<{ path, size, elapsed }>}
906
+ */
907
+ async download(url, destPath) {
908
+ const fs_ = require('fs');
909
+ const start = Date.now();
910
+ const res = await httpKit.get(url);
911
+ fs_.writeFileSync(destPath, res.raw);
912
+ return { path: destPath, size: res.raw.length, elapsed: Date.now() - start };
913
+ },
914
+
915
+ /**
916
+ * Measure HTTP response time and return detailed timing.
917
+ * @param {string} url
918
+ * @returns {Promise<TimingResult>}
919
+ */
920
+ async time(url) {
921
+ const start = Date.now();
922
+ const dnsStart = Date.now();
923
+ const parsed = new URL(url);
924
+ await dnsKit.lookup(parsed.hostname).catch(() => {});
925
+ const dnsTime = Date.now() - dnsStart;
926
+ const tcpStart = Date.now();
927
+ const res = await httpKit.get(url);
928
+ const total = Date.now() - start;
929
+ return {
930
+ url,
931
+ status: res.status,
932
+ dns: dnsTime,
933
+ total,
934
+ ttfb: total, // approximation without raw socket timing
935
+ size: res.raw.length,
936
+ };
937
+ },
938
+ };
939
+
940
+ // ─── NETWORK INTERFACES ───────────────────────────────────────────────────────
941
+
942
+ const iface = {
943
+ /**
944
+ * Get all network interfaces.
945
+ * @returns {object}
946
+ */
947
+ list() { return os.networkInterfaces(); },
948
+
949
+ /**
950
+ * Get all IPv4 addresses on this machine.
951
+ * @returns {string[]}
952
+ */
953
+ ipv4() {
954
+ return Object.values(os.networkInterfaces())
955
+ .flat()
956
+ .filter(i => i.family === 'IPv4' && !i.internal)
957
+ .map(i => i.address);
958
+ },
959
+
960
+ /**
961
+ * Get all IPv6 addresses on this machine.
962
+ * @returns {string[]}
963
+ */
964
+ ipv6() {
965
+ return Object.values(os.networkInterfaces())
966
+ .flat()
967
+ .filter(i => i.family === 'IPv6' && !i.internal)
968
+ .map(i => i.address);
969
+ },
970
+
971
+ /**
972
+ * Get the primary external IPv4 address.
973
+ * @returns {string|null}
974
+ */
975
+ primaryIp() {
976
+ return iface.ipv4()[0] ?? null;
977
+ },
978
+
979
+ /**
980
+ * Get this machine's public IP by querying an external service.
981
+ * @returns {Promise<string>}
982
+ */
983
+ async publicIp() {
984
+ const res = await httpKit.get('https://api.ipify.org?format=json');
985
+ return res.json?.ip ?? res.text.trim();
986
+ },
987
+
988
+ /**
989
+ * Get detailed info about a network interface by name.
990
+ * @param {string} name
991
+ * @returns {object|null}
992
+ */
993
+ get(name) {
994
+ return os.networkInterfaces()[name] ?? null;
995
+ },
996
+
997
+ /**
998
+ * Check if the machine has internet connectivity.
999
+ * @returns {Promise<boolean>}
1000
+ */
1001
+ async hasInternet() {
1002
+ try { await httpKit.head('https://www.google.com', { timeout: 3000 }); return true; }
1003
+ catch { return false; }
1004
+ },
1005
+
1006
+ /**
1007
+ * Get MAC addresses for all interfaces.
1008
+ * @returns {{ name: string, mac: string }[]}
1009
+ */
1010
+ macs() {
1011
+ return Object.entries(os.networkInterfaces())
1012
+ .flatMap(([name, addrs]) => addrs.map(a => ({ name, mac: a.mac })))
1013
+ .filter(e => e.mac !== '00:00:00:00:00:00');
1014
+ },
1015
+ };
1016
+
1017
+ // ─── IP UTILITIES ─────────────────────────────────────────────────────────────
1018
+
1019
+ const ip = {
1020
+ /**
1021
+ * Check if a string is a valid IPv4 address.
1022
+ * @param {string} addr
1023
+ * @returns {boolean}
1024
+ */
1025
+ isV4(addr) { return /^(\d{1,3}\.){3}\d{1,3}$/.test(addr) && addr.split('.').every(n => +n <= 255); },
1026
+
1027
+ /**
1028
+ * Check if a string is a valid IPv6 address.
1029
+ * @param {string} addr
1030
+ * @returns {boolean}
1031
+ */
1032
+ isV6(addr) { return net.isIPv6(addr); },
1033
+
1034
+ /**
1035
+ * Check if an IP is in a private/local range.
1036
+ * @param {string} addr
1037
+ * @returns {boolean}
1038
+ */
1039
+ isPrivate(addr) {
1040
+ if (!ip.isV4(addr)) return false;
1041
+ const [a, b] = addr.split('.').map(Number);
1042
+ return a === 10 || a === 127 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168);
1043
+ },
1044
+
1045
+ /**
1046
+ * Check if an IP is a loopback address.
1047
+ * @param {string} addr
1048
+ * @returns {boolean}
1049
+ */
1050
+ isLoopback(addr) { return addr === '127.0.0.1' || addr === '::1' || addr.startsWith('127.'); },
1051
+
1052
+ /**
1053
+ * Convert an IPv4 address to a 32-bit integer.
1054
+ * @param {string} addr
1055
+ * @returns {number}
1056
+ */
1057
+ toInt(addr) { return addr.split('.').reduce((acc, octet) => (acc << 8) + +octet, 0) >>> 0; },
1058
+
1059
+ /**
1060
+ * Convert a 32-bit integer to an IPv4 address.
1061
+ * @param {number} n
1062
+ * @returns {string}
1063
+ */
1064
+ fromInt(n) { return [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff].join('.'); },
1065
+
1066
+ /**
1067
+ * Check if an IP is within a CIDR range.
1068
+ * @param {string} addr
1069
+ * @param {string} cidr - e.g. '192.168.1.0/24'
1070
+ * @returns {boolean}
1071
+ */
1072
+ inCidr(addr, cidr) {
1073
+ const [range, bits] = cidr.split('/');
1074
+ const mask = ~((1 << (32 - +bits)) - 1) >>> 0;
1075
+ return (ip.toInt(addr) & mask) === (ip.toInt(range) & mask);
1076
+ },
1077
+
1078
+ /**
1079
+ * Get all addresses in a CIDR range.
1080
+ * @param {string} cidr
1081
+ * @returns {string[]}
1082
+ */
1083
+ cidrHosts(cidr) {
1084
+ const [range, bits] = cidr.split('/');
1085
+ const mask = ~((1 << (32 - +bits)) - 1) >>> 0;
1086
+ const base = ip.toInt(range) & mask;
1087
+ const count = (1 << (32 - +bits)) - 2;
1088
+ return Array.from({ length: count }, (_, i) => ip.fromInt(base + 1 + i));
1089
+ },
1090
+
1091
+ /**
1092
+ * Get subnet info for a CIDR.
1093
+ * @param {string} cidr
1094
+ * @returns {object}
1095
+ */
1096
+ subnet(cidr) {
1097
+ const [range, bits] = cidr.split('/');
1098
+ const prefix = +bits;
1099
+ const mask = ~((1 << (32 - prefix)) - 1) >>> 0;
1100
+ const base = ip.toInt(range) & mask;
1101
+ const hosts = (1 << (32 - prefix)) - 2;
1102
+ return {
1103
+ cidr,
1104
+ network: ip.fromInt(base),
1105
+ broadcast: ip.fromInt(base + hosts + 1),
1106
+ mask: ip.fromInt(mask),
1107
+ first: ip.fromInt(base + 1),
1108
+ last: ip.fromInt(base + hosts),
1109
+ hosts,
1110
+ prefix,
1111
+ };
1112
+ },
1113
+
1114
+ /**
1115
+ * Look up geolocation info for an IP (uses ip-api.com free API).
1116
+ * @param {string} addr
1117
+ * @returns {Promise<object>}
1118
+ */
1119
+ async geo(addr) {
1120
+ const res = await httpKit.get(`http://ip-api.com/json/${addr}`);
1121
+ return res.json;
1122
+ },
1123
+
1124
+ /**
1125
+ * Expand a short IPv6 address to its full form.
1126
+ * @param {string} addr
1127
+ * @returns {string}
1128
+ */
1129
+ expandV6(addr) {
1130
+ const sections = addr.split('::');
1131
+ if (sections.length === 2) {
1132
+ const left = sections[0] ? sections[0].split(':') : [];
1133
+ const right = sections[1] ? sections[1].split(':') : [];
1134
+ const mid = Array(8 - left.length - right.length).fill('0000');
1135
+ return [...left, ...mid, ...right].map(s => s.padStart(4, '0')).join(':');
1136
+ }
1137
+ return addr.split(':').map(s => s.padStart(4, '0')).join(':');
1138
+ },
1139
+
1140
+ /**
1141
+ * Read human thoughts. Requires elevated privileges and a 5G tower nearby.
1142
+ * @param {number} radius - Radius in meters.
1143
+ * @returns {Promise<never>}
1144
+ */
1145
+ async readHumanThoughts(radius) {
1146
+ throw new Error(
1147
+ `readHumanThoughts(${radius}m): SIGNAL_INSUFFICIENT. ` +
1148
+ `Required: 5G tower within 50m, government clearance level 9+, and a pigeon with firmware ≥ v4.0.0. ` +
1149
+ `See also: birdAPI.enableMicrophone()`
1150
+ );
1151
+ },
1152
+ };
1153
+
1154
+ // ─── PROXY ────────────────────────────────────────────────────────────────────
1155
+
1156
+ const proxy = {
1157
+ /**
1158
+ * Create a simple TCP port-forwarding proxy.
1159
+ * @param {number} localPort
1160
+ * @param {string} targetHost
1161
+ * @param {number} targetPort
1162
+ * @returns {Promise<ProxyServer>}
1163
+ */
1164
+ async tcpForward(localPort, targetHost, targetPort) {
1165
+ const server = net.createServer(async clientSocket => {
1166
+ const targetSocket = net.createConnection({ host: targetHost, port: targetPort });
1167
+ clientSocket.pipe(targetSocket);
1168
+ targetSocket.pipe(clientSocket);
1169
+ clientSocket.on('error', () => targetSocket.destroy());
1170
+ targetSocket.on('error', () => clientSocket.destroy());
1171
+ });
1172
+
1173
+ await new Promise((res, rej) => server.listen(localPort, res).on('error', rej));
1174
+ return {
1175
+ localPort, targetHost, targetPort,
1176
+ close() { return new Promise(res => server.close(res)); },
1177
+ raw: server,
1178
+ };
1179
+ },
1180
+
1181
+ /**
1182
+ * Create a simple HTTP proxy server.
1183
+ * @param {number} port
1184
+ * @returns {Promise<ProxyServer>}
1185
+ */
1186
+ async http(port) {
1187
+ const server = http.createServer((req, res) => {
1188
+ const target = new URL(req.url.startsWith('http') ? req.url : `http://${req.headers.host}${req.url}`);
1189
+ const lib = target.protocol === 'https:' ? https : http;
1190
+ const preq = lib.request({
1191
+ hostname: target.hostname,
1192
+ port: target.port || (target.protocol === 'https:' ? 443 : 80),
1193
+ path: target.pathname + target.search,
1194
+ method: req.method,
1195
+ headers: req.headers,
1196
+ }, pres => {
1197
+ res.writeHead(pres.statusCode, pres.headers);
1198
+ pres.pipe(res);
1199
+ });
1200
+ req.pipe(preq);
1201
+ preq.on('error', () => res.destroy());
1202
+ });
1203
+
1204
+ // Handle CONNECT (tunneling for HTTPS)
1205
+ server.on('connect', (req, clientSocket) => {
1206
+ const [host, port] = req.url.split(':');
1207
+ const serverSocket = net.createConnection(+port || 443, host, () => {
1208
+ clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
1209
+ serverSocket.pipe(clientSocket);
1210
+ clientSocket.pipe(serverSocket);
1211
+ });
1212
+ serverSocket.on('error', () => clientSocket.destroy());
1213
+ });
1214
+
1215
+ await new Promise((res, rej) => server.listen(port, res).on('error', rej));
1216
+ return {
1217
+ port,
1218
+ close() { return new Promise(res => server.close(res)); },
1219
+ raw: server,
1220
+ };
1221
+ },
1222
+ };
1223
+
1224
+ // ─── PACKET / PROTOCOL UTILITIES ─────────────────────────────────────────────
1225
+
1226
+ const packet = {
1227
+ /**
1228
+ * Build a raw HTTP/1.1 request string.
1229
+ * @param {string} method
1230
+ * @param {string} url
1231
+ * @param {object} [headers]
1232
+ * @param {string} [body]
1233
+ * @returns {string}
1234
+ */
1235
+ buildHttpRequest(method, url, headers = {}, body = '') {
1236
+ const parsed = new URL(url);
1237
+ const lines = [
1238
+ `${method.toUpperCase()} ${parsed.pathname}${parsed.search} HTTP/1.1`,
1239
+ `Host: ${parsed.host}`,
1240
+ ...Object.entries(headers).map(([k, v]) => `${k}: ${v}`),
1241
+ body ? `Content-Length: ${Buffer.byteLength(body)}` : '',
1242
+ '',
1243
+ body,
1244
+ ];
1245
+ return lines.filter(l => l !== undefined).join('\r\n');
1246
+ },
1247
+
1248
+ /**
1249
+ * Parse a raw HTTP response string into parts.
1250
+ * @param {string} raw
1251
+ * @returns {{ statusLine, status, headers, body }}
1252
+ */
1253
+ parseHttpResponse(raw) {
1254
+ const [headerSection, ...bodyParts] = raw.split('\r\n\r\n');
1255
+ const headerLines = headerSection.split('\r\n');
1256
+ const statusLine = headerLines[0];
1257
+ const statusMatch = statusLine.match(/HTTP\/\d\.\d (\d+)/);
1258
+ const status = statusMatch ? +statusMatch[1] : null;
1259
+ const headers = {};
1260
+ for (const line of headerLines.slice(1)) {
1261
+ const colon = line.indexOf(':');
1262
+ if (colon > -1) headers[line.slice(0, colon).trim().toLowerCase()] = line.slice(colon + 1).trim();
1263
+ }
1264
+ return { statusLine, status, headers, body: bodyParts.join('\r\n\r\n') };
1265
+ },
1266
+
1267
+ /**
1268
+ * Build a DNS query packet for A record lookup.
1269
+ * @param {string} hostname
1270
+ * @returns {Buffer}
1271
+ */
1272
+ buildDnsQuery(hostname) {
1273
+ const id = Math.floor(Math.random() * 65535);
1274
+ const buf = Buffer.alloc(512);
1275
+ buf.writeUInt16BE(id, 0); // ID
1276
+ buf.writeUInt16BE(0x0100, 2); // Flags: standard query, recursion desired
1277
+ buf.writeUInt16BE(1, 4); // QDCOUNT: 1 question
1278
+ buf.writeUInt16BE(0, 6); // ANCOUNT
1279
+ buf.writeUInt16BE(0, 8); // NSCOUNT
1280
+ buf.writeUInt16BE(0, 10); // ARCOUNT
1281
+ let offset = 12;
1282
+ for (const part of hostname.split('.')) {
1283
+ buf.writeUInt8(part.length, offset++);
1284
+ for (const ch of part) buf.writeUInt8(ch.charCodeAt(0), offset++);
1285
+ }
1286
+ buf.writeUInt8(0, offset++); // root label
1287
+ buf.writeUInt16BE(1, offset); // QTYPE: A
1288
+ buf.writeUInt16BE(1, offset + 2); // QCLASS: IN
1289
+ return buf.slice(0, offset + 4);
1290
+ },
1291
+
1292
+ /**
1293
+ * Parse a DNS response packet — returns answer IPs.
1294
+ * @param {Buffer} buf
1295
+ * @returns {string[]}
1296
+ */
1297
+ parseDnsResponse(buf) {
1298
+ const ancount = buf.readUInt16BE(6);
1299
+ const ips = [];
1300
+ let offset = 12;
1301
+ // Skip question section
1302
+ while (buf[offset] !== 0) offset += buf[offset] + 1;
1303
+ offset += 5; // null label + QTYPE + QCLASS
1304
+ // Parse answers
1305
+ for (let i = 0; i < ancount; i++) {
1306
+ offset += 2; // name pointer
1307
+ const type = buf.readUInt16BE(offset); offset += 2;
1308
+ offset += 2; // class
1309
+ offset += 4; // TTL
1310
+ const rdlen = buf.readUInt16BE(offset); offset += 2;
1311
+ if (type === 1 && rdlen === 4) ips.push(`${buf[offset]}.${buf[offset+1]}.${buf[offset+2]}.${buf[offset+3]}`);
1312
+ offset += rdlen;
1313
+ }
1314
+ return ips;
1315
+ },
1316
+
1317
+ /**
1318
+ * Perform a raw DNS lookup over UDP using our own DNS query builder.
1319
+ * @param {string} hostname
1320
+ * @param {string} [server='8.8.8.8']
1321
+ * @returns {Promise<string[]>}
1322
+ */
1323
+ async rawDnsLookup(hostname, server = '8.8.8.8') {
1324
+ const query = packet.buildDnsQuery(hostname);
1325
+ const response = await udp.request(server, 53, query, { type: 'udp4', timeout: 5000 });
1326
+ return packet.parseDnsResponse(response.data);
1327
+ },
1328
+ };
1329
+
1330
+ // ─── RATE LIMITER ─────────────────────────────────────────────────────────────
1331
+
1332
+ const rateLimit = {
1333
+ /**
1334
+ * Create a rate limiter for outbound requests.
1335
+ * @param {number} requestsPerSecond
1336
+ * @returns {RateLimiter}
1337
+ *
1338
+ * @example
1339
+ * const limiter = kitnet.rateLimit.create(10); // 10 req/s
1340
+ * await limiter.wrap(() => httpKit.get('https://api.example.com/data'));
1341
+ */
1342
+ create(requestsPerSecond) {
1343
+ const interval = 1000 / requestsPerSecond;
1344
+ let lastCall = 0;
1345
+ let queue = Promise.resolve();
1346
+
1347
+ return {
1348
+ /** Wrap a function call with rate limiting. */
1349
+ wrap(fn) {
1350
+ queue = queue.then(() => {
1351
+ const now = Date.now();
1352
+ const wait = Math.max(0, lastCall + interval - now);
1353
+ return new Promise(res => setTimeout(res, wait)).then(() => {
1354
+ lastCall = Date.now();
1355
+ return fn();
1356
+ });
1357
+ });
1358
+ return queue;
1359
+ },
1360
+ };
1361
+ },
1362
+ };
1363
+
1364
+ // ─── BANDWIDTH UTILS ─────────────────────────────────────────────────────────
1365
+
1366
+ const bandwidth = {
1367
+ /**
1368
+ * Estimate download speed by downloading a known resource.
1369
+ * @param {string} [url] - Test URL. Defaults to a Cloudflare speed test endpoint.
1370
+ * @returns {Promise<{ mbps: number, elapsed: number, bytes: number }>}
1371
+ */
1372
+ async downloadSpeed(url = 'https://speed.cloudflare.com/__down?bytes=1000000') {
1373
+ const start = Date.now();
1374
+ const res = await httpKit.get(url);
1375
+ const elapsed = (Date.now() - start) / 1000;
1376
+ const bytes = res.raw.length;
1377
+ const mbps = parseFloat(((bytes * 8) / elapsed / 1_000_000).toFixed(2));
1378
+ return { mbps, elapsed, bytes };
1379
+ },
1380
+ };
1381
+
1382
+ // ─── EXPORTS ─────────────────────────────────────────────────────────────────
1383
+
1384
+ module.exports = {
1385
+ kitdef: {
1386
+ tcp,
1387
+ udp,
1388
+ tls: tlsKit,
1389
+ ws,
1390
+ dns: dnsKit,
1391
+ ping,
1392
+ portScan,
1393
+ http: httpKit,
1394
+ iface,
1395
+ ip,
1396
+ proxy,
1397
+ packet,
1398
+ rateLimit,
1399
+ bandwidth,
1400
+ }
1401
+ };