hive-p2p 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ const PRIME32_1 = 2654435761;
2
+ const PRIME32_2 = 2246822519;
3
+ const PRIME32_3 = 3266489917;
4
+ const PRIME32_4 = 668265263;
5
+ const PRIME32_5 = 374761393;
6
+ let encoder;
7
+ /** @param input - byte array or string @param seed - optional seed (32-bit unsigned); */
8
+ export function xxHash32(input, seed = 0) {
9
+ const buffer = typeof input === 'string' ? (encoder ??= new TextEncoder()).encode(input) : input;
10
+ const b = buffer;
11
+ let acc = (seed + PRIME32_5) & 0xffffffff;
12
+ let offset = 0;
13
+ if (b.length >= 16) {
14
+ const accN = [
15
+ (seed + PRIME32_1 + PRIME32_2) & 0xffffffff,
16
+ (seed + PRIME32_2) & 0xffffffff,
17
+ (seed + 0) & 0xffffffff,
18
+ (seed - PRIME32_1) & 0xffffffff,
19
+ ];
20
+ const b = buffer;
21
+ const limit = b.length - 16;
22
+ let lane = 0;
23
+ for (offset = 0; (offset & 0xfffffff0) <= limit; offset += 4) {
24
+ const i = offset;
25
+ const laneN0 = b[i + 0] + (b[i + 1] << 8);
26
+ const laneN1 = b[i + 2] + (b[i + 3] << 8);
27
+ const laneNP = laneN0 * PRIME32_2 + ((laneN1 * PRIME32_2) << 16);
28
+ let acc = (accN[lane] + laneNP) & 0xffffffff;
29
+ acc = (acc << 13) | (acc >>> 19);
30
+ const acc0 = acc & 0xffff;
31
+ const acc1 = acc >>> 16;
32
+ accN[lane] = (acc0 * PRIME32_1 + ((acc1 * PRIME32_1) << 16)) & 0xffffffff;
33
+ lane = (lane + 1) & 0x3;
34
+ }
35
+ acc =
36
+ (((accN[0] << 1) | (accN[0] >>> 31)) +
37
+ ((accN[1] << 7) | (accN[1] >>> 25)) +
38
+ ((accN[2] << 12) | (accN[2] >>> 20)) +
39
+ ((accN[3] << 18) | (accN[3] >>> 14))) &
40
+ 0xffffffff;
41
+ }
42
+ acc = (acc + buffer.length) & 0xffffffff;
43
+ const limit = buffer.length - 4;
44
+ for (; offset <= limit; offset += 4) {
45
+ const i = offset;
46
+ const laneN0 = b[i + 0] + (b[i + 1] << 8);
47
+ const laneN1 = b[i + 2] + (b[i + 3] << 8);
48
+ const laneP = laneN0 * PRIME32_3 + ((laneN1 * PRIME32_3) << 16);
49
+ acc = (acc + laneP) & 0xffffffff;
50
+ acc = (acc << 17) | (acc >>> 15);
51
+ acc = ((acc & 0xffff) * PRIME32_4 + (((acc >>> 16) * PRIME32_4) << 16)) & 0xffffffff;
52
+ }
53
+ for (; offset < b.length; ++offset) {
54
+ const lane = b[offset];
55
+ acc = acc + lane * PRIME32_5;
56
+ acc = (acc << 11) | (acc >>> 21);
57
+ acc = ((acc & 0xffff) * PRIME32_1 + (((acc >>> 16) * PRIME32_1) << 16)) & 0xffffffff;
58
+ }
59
+ acc = acc ^ (acc >>> 15);
60
+ acc = (((acc & 0xffff) * PRIME32_2) & 0xffffffff) + (((acc >>> 16) * PRIME32_2) << 16);
61
+ acc = acc ^ (acc >>> 13);
62
+ acc = (((acc & 0xffff) * PRIME32_3) & 0xffffffff) + (((acc >>> 16) * PRIME32_3) << 16);
63
+ acc = acc ^ (acc >>> 16);
64
+ return acc < 0 ? acc + 4294967296 : acc;
65
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "hive-p2p",
3
+ "version": "1.0.0",
4
+ "description": "P2P networking library for browser and Node.js with WebRTC",
5
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "browser": "./browser.mjs",
8
+ "exports": {
9
+ ".": {
10
+ "browser": "./browser.mjs",
11
+ "node": "./index.mjs",
12
+ "default": "./index.mjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "core/",
17
+ "libs/",
18
+ "services/",
19
+ "index.mjs",
20
+ "browser.mjs"
21
+ ],
22
+ "dependencies": {
23
+ "@noble/ed25519": "3.0.0",
24
+ "@noble/hashes": "2.0.0",
25
+ "argon2": "~0.44.0",
26
+ "express": "~5.1.0",
27
+ "node-pre-gyp": "~0.17.0",
28
+ "simple-peer": "~9.11.1",
29
+ "socket.io-client": "~4.8.1",
30
+ "socket.io": "~4.8.1",
31
+ "wrtc": "~0.4.7",
32
+ "ws": "~8.18.3"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/Seigneur-Machiavel/hive-p2p.git"
37
+ },
38
+ "keywords": ["p2p", "webrtc", "networking", "browser", "nodejs", "gossip", "mesh"],
39
+ "author": "Seigneur-Machiavel",
40
+ "license": "MIT",
41
+ "scripts": {
42
+ "test": "echo \"Error: no test specified\" && exit 1"
43
+ }
44
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Synchronized Network Clock
3
+ * Simple, efficient NTP-based time synchronization
4
+ */
5
+
6
+ export class Clock {
7
+ verbose;
8
+ mockMode; // if true, use local time without sync
9
+ static #instance = null;
10
+
11
+ #offset = null; // ms difference from local time
12
+ #syncing = false;
13
+ #lastSync = 0;
14
+ #sources = ['time.google.com', 'time.cloudflare.com', 'pool.ntp.org'];
15
+
16
+ constructor(verbose = 0, mockMode = false) {
17
+ this.verbose = verbose;
18
+ this.mockMode = mockMode;
19
+ if (Clock.#instance) return Clock.#instance;
20
+ else Clock.#instance = this;
21
+ }
22
+
23
+ // PUBLIC API
24
+ static get instance() { return Clock.#instance || new Clock(); }
25
+ static get time() { return Clock.instance.time; } // Sync API - returns current synchronized time or null
26
+ get time() {
27
+ if (this.mockMode) return Date.now();
28
+ if (this.#offset === null) return null;
29
+ return Date.now() + Math.round(this.#offset);
30
+ }
31
+ async sync(verbose) { // Force synchronization - returns promise with synchronized time
32
+ if (verbose !== undefined) this.verbose = verbose;
33
+ if (this.mockMode) return Date.now(); // Bypass sync in mock mode
34
+
35
+ if (this.#syncing) { // Wait for current sync to complete
36
+ while (this.#syncing) await new Promise(resolve => setTimeout(resolve, 50));
37
+ return this.time;
38
+ }
39
+
40
+ this.#syncing = true;
41
+
42
+ try {
43
+ const samples = await this.#fetchTimeSamples();
44
+ if (samples.length === 0) {
45
+ console.warn('[Clock] All NTP sources failed, using local time');
46
+ this.#offset = 0;
47
+ return this.time;
48
+ }
49
+
50
+ this.#offset = this.#calculateOffset(samples);
51
+ this.#lastSync = Date.now();
52
+
53
+ // Continue refining in background if we got partial results
54
+ if (samples.length < this.#sources.length) setTimeout(() => this.#backgroundRefine(), 100);
55
+ return this.time;
56
+ } catch (error) {
57
+ console.error('[Clock] Sync failed:', error);
58
+ this.#offset = 0; // Fallback to local time
59
+ return this.time;
60
+ } finally { this.#syncing = false; }
61
+ }
62
+ get status() { // Get sync status info
63
+ if (this.mockMode) return { synchronized: true, syncing: false, offset: 0, lastSync: Date.now(), age: 0 };
64
+ return {
65
+ synchronized: this.#offset !== null,
66
+ syncing: this.#syncing,
67
+ offset: this.#offset,
68
+ lastSync: this.#lastSync,
69
+ age: this.#lastSync ? Date.now() - this.#lastSync : null
70
+ };
71
+ }
72
+
73
+ // PRIVATE METHODS
74
+ async #fetchTimeSamples() { // Fetch time samples from all sources in parallel
75
+ const promises = this.#sources.map(source => this.#fetchTimeFromSource(source));
76
+ const results = await Promise.allSettled(promises);
77
+ const samples = [];
78
+ for (const result of results) if (result.status === 'fulfilled') samples.push(result.value);
79
+ return samples;
80
+ }
81
+ /** @param {'time.google.com' | 'time.cloudflare.com' | 'pool.ntp.org'} source */
82
+ async #fetchTimeFromSource(source) { // Fetch time from a single NTP source
83
+ const controller = new AbortController();
84
+ const timeoutId = setTimeout(() => controller.abort(), 2000); // 2s timeout
85
+
86
+ try {
87
+ const startTime = Date.now();
88
+ const response = await fetch(`https://${source}`, { method: 'HEAD', signal: controller.signal, cache: 'no-cache' });
89
+ const networkLatency = (Date.now() - startTime) / 2; // Rough RTT/2
90
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
91
+
92
+ const serverTime = new Date(response.headers.get('date')).getTime();
93
+ if (isNaN(serverTime)) throw new Error('Invalid date header');
94
+
95
+ return {
96
+ source,
97
+ serverTime: serverTime + networkLatency, // Compensate for network delay
98
+ localTime: Date.now(),
99
+ latency: networkLatency * 2
100
+ };
101
+ } finally { clearTimeout(timeoutId); }
102
+ }
103
+ /** @param {Array<{serverTime: number, localTime: number, latency: number}>} samples */
104
+ #calculateOffset(samples) { // Calculate offset from multiple samples
105
+ if (samples.length === 1) return samples[0].serverTime - samples[0].localTime;
106
+ samples.sort((a, b) => a.latency - b.latency); // Sort by latency, prefer lower latency sources
107
+
108
+ const offsets = [];
109
+ for (const sample of samples) offsets.push(sample.serverTime - sample.localTime);
110
+ offsets.sort((a, b) => a - b); // Use median to filter outliers
111
+ const mid = Math.floor(offsets.length / 2);
112
+
113
+ if (offsets.length % 2 === 0) return (offsets[mid - 1] + offsets[mid]) / 2;
114
+ return offsets[mid];
115
+ }
116
+ async #backgroundRefine() { // Background refinement after initial sync
117
+ if (this.#syncing) return;
118
+
119
+ try {
120
+ const samples = await this.#fetchTimeSamples();
121
+ if (samples.length === 0) return; // All failed
122
+
123
+ // Only update if change is significant (> 100ms)
124
+ const newOffset = this.#calculateOffset(samples);
125
+ if (Math.abs(newOffset - this.#offset) > 100) this.#offset = newOffset;
126
+ } catch (error) { if (this.verbose) console.warn('[Clock] Background refine failed:', error); }
127
+ }
128
+ }
129
+
130
+
131
+ export async function CLOCK_TEST() { // DEBUG TEST WHILE RUNNING AS STANDALONE
132
+ const startTime = Date.now();
133
+ const clock = new Clock();
134
+ clock.sync().then(() => {
135
+ console.log('Synchronized in: ', Date.now() - startTime, 'ms');
136
+ console.log('Synchronized time:', new Date(clock.time).toISOString());
137
+ console.log('Clock status:', clock.status);
138
+ }).catch(console.error);
139
+
140
+ while (true) {
141
+ console.log('Clock status:', clock.status);
142
+ await new Promise(r => setTimeout(r, 1000));
143
+ }
144
+ }
@@ -0,0 +1,64 @@
1
+ export class Converter {
2
+ IS_BROWSER = typeof window !== 'undefined';
3
+ FROMBASE64_AVAILABLE = typeof Uint8Array.fromBase64 === 'function';
4
+ textEncoder = new TextEncoder();
5
+ textDecoder = new TextDecoder();
6
+ buffer2 = new ArrayBuffer(2); view2 = new DataView(this.buffer2);
7
+ buffer4 = new ArrayBuffer(4); view4 = new DataView(this.buffer4);
8
+ buffer8 = new ArrayBuffer(8); view8 = new DataView(this.buffer8);
9
+ hexMap = { '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15, 'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, 'f': 15 };
10
+
11
+ // ... TO BYTES
12
+ /** Number should be between 0 and 65535 @param {number} num - Integer to convert to 2 bytes Uint8Array */
13
+ numberTo2Bytes(num) { this.view2.setUint16(0, num, true); return new Uint8Array(this.buffer2); }
14
+ /** Number should be between 0 and 4294967295 @param {number} num - Integer to convert to 4 bytes Uint8Array */
15
+ numberTo4Bytes(num) { this.view4.setUint32(0, num, true); return new Uint8Array(this.buffer4); }
16
+ /** Number should be between 0 and 18446744073709551615 @param {number} num - Integer to convert to 8 bytes Uint8Array */
17
+ numberTo8Bytes(num) { this.view8.setBigUint64(0, BigInt(num), true); return new Uint8Array(this.buffer8); }
18
+ stringToBytes(str = 'toto') { return this.textEncoder.encode(str); }
19
+ /** @param {string} hex - Hex string to convert to Uint8Array */
20
+ hexToBytes(hex) {
21
+ const length = hex.length / 2;
22
+ const uint8Array = new Uint8Array(length);
23
+ for (let i = 0, j = 0; i < length; ++i, j += 2) uint8Array[i] = (this.hexMap[hex[j]] << 4) + this.hexMap[hex[j + 1]];
24
+ return uint8Array;
25
+ }
26
+ /** Base64 string to convert to Uint8Array @param {string} base64 @returns {Uint8Array} */
27
+ base64toBytes(base64) {
28
+ if (!this.IS_BROWSER) return new Uint8Array(Buffer.from(base64, 'base64'));
29
+ if (this.FROMBASE64_AVAILABLE) return Uint8Array.fromBase64(base64);
30
+ const binaryString = atob(base64);
31
+ const bytes = new Uint8Array(binaryString.length);
32
+ for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
33
+ return bytes;
34
+ }
35
+ // BYTES TO ...
36
+ /** @param {Uint8Array} uint8Array - Uint8Array to convert to number */
37
+ bytes2ToNumber(uint8Array) { for (let i = 0; i < 2; i++) this.view2.setUint8(i, uint8Array[i]); return this.view2.getUint16(0, true); }
38
+ /** @param {Uint8Array} uint8Array - Uint8Array to convert to number */
39
+ bytes4ToNumber(uint8Array) { for (let i = 0; i < 4; i++) this.view4.setUint8(i, uint8Array[i]); return this.view4.getUint32(0, true); }
40
+ /** @param {Uint8Array} uint8Array - Uint8Array to convert to number */
41
+ bytes8ToNumber(uint8Array) { for (let i = 0; i < 8; i++) this.view8.setUint8(i, uint8Array[i]); return Number(this.view8.getBigUint64(0, true)); }
42
+ /** @param {Uint8Array} uint8Array - Uint8Array to convert to string */
43
+ bytesToString(uint8Array) { return this.textDecoder.decode(uint8Array); }
44
+ /** @param {Uint8Array} uint8Array - Uint8Array to convert to string */
45
+ static bytesToHex(uint8Array, minLength = 0) {
46
+ let hexStr = '';
47
+ for (const byte of uint8Array) hexStr += byte < 16 ? `0${byte.toString(16)}` : byte.toString(16);
48
+ if (minLength > 0) { hexStr = hexStr.padStart(minLength, '0'); }
49
+ return hexStr;
50
+ }
51
+ /** @param {Uint8Array} uint8Array - Uint8Array to convert to string */
52
+ bytesToHex(uint8Array, minLength = 0) { return Converter.bytesToHex(uint8Array, minLength); }
53
+ // OTHERS
54
+ /** @param {string} hex - Hex string to convert to bits @param {'string' | 'arrayOfString' | 'arrayOfNumbers'} format - Output format, default: string */
55
+ static hexToBits(hex = 'ffffff', format = 'string') {
56
+ let bitsString = []; // WE USE STRING METHODS FOR PERFORMANCE (number '0' = 8 Bytes | string '0' = 1 Byte)
57
+ if (format === 'arrayOfNumbers') for (let i = 0; i < hex.length; i++) bitsString.push(...(parseInt(hex[i], 16).toString(2).padStart(4, '0').split('').map(b => parseInt(b, 10))));
58
+ else for (let i = 0; i < hex.length; i++) bitsString.push(parseInt(hex[i], 16).toString(2).padStart(4, '0'));
59
+ return format === 'string' ? bitsString.join('') : bitsString;
60
+ }
61
+ /** @param {Uint8Array} uint8Array - Uint8Array to convert to bits @param {'string' | 'arrayOfString' | 'arrayOfNumbers'} format - Output format, default: string */
62
+ static bytesToBits(uint8Array, format = 'string') { return this.hexToBits(this.bytesToHex(uint8Array), format); }
63
+ static ipToInt(ip = '192.168.0.1') { return ip.split('.').reduce((int, oct) => (int << 8) + parseInt(oct, 10), 0); }
64
+ }
@@ -0,0 +1,83 @@
1
+ import { Converter } from './converter.mjs';
2
+ const IS_BROWSER = typeof window !== 'undefined';
3
+
4
+ // ED25519 EXPOSURE NODEJS/BROWSER COMPATIBLE ---------------------------------
5
+ const [ed_, {sha512}] = await Promise.all([
6
+ import(IS_BROWSER ? 'https://unpkg.com/@noble/ed25519@3.0.0/index.js' : '@noble/ed25519'),
7
+ import(IS_BROWSER ? 'https://unpkg.com/@noble/hashes@2.0.0/sha2.js' : '@noble/hashes/sha2.js')
8
+ ]);
9
+
10
+ /** @type {import('@noble/ed25519')} */
11
+ const ed25519 = ed_;
12
+ ed25519.hashes.sha512 = sha512;
13
+ export { ed25519 };
14
+ //-----------------------------------------------------------------------------
15
+
16
+ // ARGON2 EXPOSURE NODEJS/BROWSER COMPATIBLE ----------------------------------
17
+ export class Argon2Unified {
18
+ converter = new Converter();
19
+ /** @type {import('argon2')} */ argon2;
20
+
21
+ /** This function hashes a password using Argon2 - Browser/NodeJS unified
22
+ * @param {string} pass - Password to hash
23
+ * @param {string} salt - Salt to use for the hash
24
+ * @param {number} [mem] - Memory usage in KiB, default: 2**16 = 65_536 (64 MiB) | RECOMMENDED: 2**16
25
+ * @param {number} [time] - Time cost in iterations, default: 1
26
+ * @param {number} [parallelism] - Number of threads to use, default: 1
27
+ * @param {number} [type] - 0: Argon2d, 1: Argon2i, 2: Argon2id, default: 2 (Argon2id)
28
+ * @param {number} [hashLen] - Length of the hash in bytes, default: 32 */
29
+ hash = async (pass, salt, mem = 2**16, time = 1, parallelism = 1, type = 2, hashLen = 32) => {
30
+ const params = this.#createArgon2Params(pass, salt, time, mem, parallelism, type, hashLen);
31
+ const argon2Lib = await this.getArgon2Lib();
32
+ const hashResult = IS_BROWSER ? await argon2Lib.hash(params) : await argon2Lib.hash(pass, params);
33
+ if (!hashResult) return false;
34
+
35
+ const encoded = hashResult.encoded || hashResult;
36
+ const result = this.#standardizeArgon2FromEncoded(encoded);
37
+ if (!result) return false;
38
+
39
+ return result;
40
+ }
41
+ async getArgon2Lib() {
42
+ if (this.argon2) return this.argon2;
43
+
44
+ if (!IS_BROWSER) { try {
45
+ const a = await import('argon2');
46
+ this.argon2 = a;
47
+ } catch (error) { throw new Error('Please install argon2 package: npm install argon2'); } }
48
+ if (this.argon2) return this.argon2;
49
+
50
+ try { if (argon2) {
51
+ console.log('Argon2 loaded as a global variable');
52
+ this.argon2 = argon2;
53
+ return this.argon2;
54
+ }} catch (error) { }
55
+ if (this.argon2) return this.argon2;
56
+
57
+ console.log('trying import argon2 ES6 and inject in window');
58
+ const argon2ES6 = await import('../libs/argon2-ES6.min.mjs');
59
+ window.argon2 = argon2ES6.default; // EXPOSE TO GLOBAL SCOPE
60
+ this.argon2 = argon2ES6.default;
61
+ return this.argon2;
62
+ };
63
+ #createArgon2Params(pass = "averylongpassword123456", salt = "saltsaltsaltsaltsalt", time = 1, mem = 2**10, parallelism = 1, type = 2, hashLen = 32) {
64
+ const fixedSalt = salt.padEnd(20, '0').substring(0, 16); // 16 bytes minimum
65
+ return {
66
+ type, pass, parallelism,
67
+ time, timeCost: time, // we preserve both for compatibility
68
+ mem, memoryCost: mem, // we preserve both for compatibility
69
+ hashLen, hashLength: hashLen, // we preserve both for compatibility
70
+ salt: IS_BROWSER ? fixedSalt : Buffer.from(fixedSalt),
71
+ };
72
+ }
73
+ #standardizeArgon2FromEncoded(encoded = '$argon2id$v=19$m=1048576,t=1,p=1$c2FsdHNhbHRzYWx0c2FsdHNhbHQ$UamPN/XTTX4quPewQNw4/s3y1JJeS22cRroh5l7OTMM') {
74
+ const base64 = encoded.split('$').pop();
75
+ const hash = this.converter.base64toBytes(base64);
76
+ const hex = this.converter.bytesToHex(hash);
77
+ /** @type {string} */
78
+ const bitsString = Converter.hexToBits(hex, 'string');
79
+ if (!bitsString) return false;
80
+ return { encoded, hash, hex, bitsString };
81
+ }
82
+ }
83
+ //-----------------------------------------------------------------------------