the-invisible-billion-cli 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.
- package/LICENSE +21 -0
- package/README.md +371 -0
- package/bin/ib.js +90 -0
- package/package.json +28 -0
- package/src/cli/commands/file.js +20 -0
- package/src/cli/commands/scan.js +50 -0
- package/src/cli/commands/send.js +113 -0
- package/src/cli/commands/start.js +126 -0
- package/src/cli/commands/status.js +43 -0
- package/src/cli/commands/stop.js +15 -0
- package/src/cli/commands/sync.js +31 -0
- package/src/cli/ipc.js +127 -0
- package/src/crypto/encrypt.js +100 -0
- package/src/crypto/keys.js +95 -0
- package/src/daemon/epidemic.js +103 -0
- package/src/daemon/index.js +683 -0
- package/src/daemon/network.js +84 -0
- package/src/daemon/tcp.js +200 -0
- package/src/daemon/udp.js +282 -0
- package/src/db/cleanup.js +39 -0
- package/src/db/index.js +74 -0
- package/src/db/messages.js +213 -0
- package/src/identity/index.js +92 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { openDb } = require('./index'); //importing database
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TTL_DAYS = 7;
|
|
6
|
+
|
|
7
|
+
//inserting a message in databse
|
|
8
|
+
function insertMessage(msg) {
|
|
9
|
+
const db = openDb();
|
|
10
|
+
const now = Date.now();
|
|
11
|
+
const ttl = msg.ttl || now + DEFAULT_TTL_DAYS * 24 * 60 * 60 * 1000;//if message has ttl, if not add 7 days later
|
|
12
|
+
|
|
13
|
+
//preparing sql query
|
|
14
|
+
const stmt = db.prepare(`
|
|
15
|
+
INSERT INTO messages (id, from_identity, destination, payload, status, hop_count, ttl, created_at)
|
|
16
|
+
VALUES (@id, @from_identity, @destination, @payload, @status, @hop_count, @ttl, @created_at)
|
|
17
|
+
`);
|
|
18
|
+
|
|
19
|
+
stmt.run({
|
|
20
|
+
id: msg.id,
|
|
21
|
+
from_identity: msg.from_identity,
|
|
22
|
+
destination: msg.destination,
|
|
23
|
+
payload: msg.payload,
|
|
24
|
+
status: msg.status || 'undelivered',
|
|
25
|
+
hop_count: msg.hop_count || 0,
|
|
26
|
+
ttl,
|
|
27
|
+
created_at: now,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//retrieves the message from message id
|
|
32
|
+
function getMessageById(id) {
|
|
33
|
+
const db = openDb();
|
|
34
|
+
return db.prepare('SELECT * FROM messages WHERE id = ?').get(id) || null; //idhar run kyu nahi use kiya?
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Retrieve all messages that are undelivered or intransit.
|
|
39
|
+
* Used for epidemic forwarding.
|
|
40
|
+
*/
|
|
41
|
+
function getAllPending() {
|
|
42
|
+
const db = openDb();
|
|
43
|
+
return db.prepare(`
|
|
44
|
+
SELECT * FROM messages
|
|
45
|
+
WHERE status IN ('undelivered', 'intransit')
|
|
46
|
+
AND hop_count < 20
|
|
47
|
+
AND ttl > ?
|
|
48
|
+
`).all(Date.now()); //same here, idhar bhi run use nahi kiya??
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//update status to delivered
|
|
52
|
+
function markDelivered(id) {
|
|
53
|
+
const db = openDb();
|
|
54
|
+
db.prepare(`UPDATE messages SET status = 'delivered' WHERE id = ?`).run(id);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//update status to transmit
|
|
58
|
+
function markInTransit(id) {
|
|
59
|
+
const db = openDb();
|
|
60
|
+
db.prepare(`UPDATE messages SET status = 'intransit' WHERE id = ?`).run(id);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//Increment hop count for a message.
|
|
64
|
+
function incrementHop(id) {
|
|
65
|
+
const db = openDb();
|
|
66
|
+
db.prepare(`UPDATE messages SET hop_count = hop_count + 1 WHERE id = ?`).run(id);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//when a relay sends a message to their peer, used when message need re-encryption
|
|
70
|
+
function updateMessagePayload(id, newPayload) {
|
|
71
|
+
const db = openDb();
|
|
72
|
+
db.prepare(`UPDATE messages SET payload = ? WHERE id = ?`).run(newPayload, id);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get all pending messages destined for a specific identity.
|
|
77
|
+
* Used when a peer is discovered via UDP to trigger targeted delivery.
|
|
78
|
+
*/
|
|
79
|
+
function getMessagesForDestination(destination) {
|
|
80
|
+
const db = openDb();
|
|
81
|
+
return db.prepare(`
|
|
82
|
+
SELECT * FROM messages
|
|
83
|
+
WHERE destination = ?
|
|
84
|
+
AND status IN ('undelivered', 'intransit')
|
|
85
|
+
AND hop_count < 20
|
|
86
|
+
AND ttl > ?
|
|
87
|
+
`).all(destination, Date.now());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
*
|
|
92
|
+
* Upsert a message received from a peer (for epidemic relay).
|
|
93
|
+
* If message already exists (same ID), update hop_count if incoming is lower.
|
|
94
|
+
* This prevents re-inserting messages we already have and avoids loops.
|
|
95
|
+
*/
|
|
96
|
+
function upsertMessage(msg) {
|
|
97
|
+
const db = openDb();
|
|
98
|
+
const existing = getMessageById(msg.id);
|
|
99
|
+
if (existing) {
|
|
100
|
+
// Already have it ā only update hop count if the incoming route is shorter
|
|
101
|
+
if (msg.hop_count < existing.hop_count) {
|
|
102
|
+
db.prepare(`UPDATE messages SET hop_count = ? WHERE id = ?`)
|
|
103
|
+
.run(msg.hop_count, msg.id);
|
|
104
|
+
}
|
|
105
|
+
return 'exists';
|
|
106
|
+
}
|
|
107
|
+
insertMessage(msg);
|
|
108
|
+
return 'inserted';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Upsert a known peer, their public key, and current IP. 3 things can happen:
|
|
113
|
+
* if peer already exists, update their public key and IP if changed, and refresh last_seen.
|
|
114
|
+
* if peer is new, insert them with current timestamp.
|
|
115
|
+
* if peer goes offline, we can nullify their IP but keep their public key for future re-encryption when they come back online.
|
|
116
|
+
*/
|
|
117
|
+
function upsertPeer(identity, publicKeyPem, ip) {
|
|
118
|
+
const db = openDb();
|
|
119
|
+
db.prepare(`
|
|
120
|
+
INSERT INTO peers (identity, public_key, ip, last_seen)
|
|
121
|
+
VALUES (?, ?, ?, ?)
|
|
122
|
+
ON CONFLICT(identity) DO UPDATE SET
|
|
123
|
+
public_key = excluded.public_key,
|
|
124
|
+
ip = COALESCE(excluded.ip, peers.ip),
|
|
125
|
+
last_seen = excluded.last_seen
|
|
126
|
+
`).run(identity, publicKeyPem, ip || null, Date.now());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Retrieve a peer's public key by their identity string.
|
|
131
|
+
* Returns null if unknown.
|
|
132
|
+
*/
|
|
133
|
+
function getPeerPublicKey(identity) {
|
|
134
|
+
const db = openDb();
|
|
135
|
+
const row = db.prepare('SELECT public_key FROM peers WHERE identity = ?').get(identity);
|
|
136
|
+
return row ? row.public_key : null; // if row exists, return public_key, else return null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Retrieve a full peer record (identity, public_key, ip, last_seen).
|
|
141
|
+
* Returns null if unknown.
|
|
142
|
+
*/
|
|
143
|
+
function getPeerByIdentity(identity) {
|
|
144
|
+
const db = openDb();
|
|
145
|
+
return db.prepare('SELECT * FROM peers WHERE identity = ?').get(identity) || null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get all known active peers (seen within last 5 minutes).
|
|
150
|
+
*/
|
|
151
|
+
function getActivePeers(withinMs = 5 * 60 * 1000) {
|
|
152
|
+
const db = openDb();
|
|
153
|
+
return db.prepare('SELECT * FROM peers WHERE last_seen > ?').all(Date.now() - withinMs);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get all peers, optionally filtered by recency.
|
|
158
|
+
* @param {number} [withinMs] - Only return peers seen within this many ms. 0 = all peers.
|
|
159
|
+
*/
|
|
160
|
+
function getAllPeers(withinMs = 0) {
|
|
161
|
+
const db = openDb();
|
|
162
|
+
if (withinMs > 0) {
|
|
163
|
+
const cutoff = Date.now() - withinMs;
|
|
164
|
+
return db.prepare('SELECT identity, ip, last_seen FROM peers WHERE last_seen > ? ORDER BY last_seen DESC').all(cutoff);
|
|
165
|
+
}
|
|
166
|
+
return db.prepare('SELECT identity, ip, last_seen FROM peers ORDER BY last_seen DESC').all();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Mark a peer as offline (nullify IP) but keep their public key for future re-encryption.
|
|
171
|
+
* @param {string} identity
|
|
172
|
+
*/
|
|
173
|
+
function markPeerOffline(identity) {
|
|
174
|
+
const db = openDb();
|
|
175
|
+
db.prepare('UPDATE peers SET ip = NULL WHERE identity = ?').run(identity);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Clear all peer IPs on daemon startup (stale from previous session).
|
|
179
|
+
function clearAllPeerIPs() {
|
|
180
|
+
const db = openDb();
|
|
181
|
+
db.prepare('UPDATE peers SET ip = NULL').run();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Remove peers not seen within a given threshold.
|
|
186
|
+
* @param {number} olderThanMs - Remove peers with last_seen older than this many ms ago
|
|
187
|
+
* @returns {number} Number of peers removed
|
|
188
|
+
*/
|
|
189
|
+
function removeInactivePeers(olderThanMs) {
|
|
190
|
+
const db = openDb();
|
|
191
|
+
const cutoff = Date.now() - olderThanMs;
|
|
192
|
+
return db.prepare('DELETE FROM peers WHERE last_seen < ?').run(cutoff).changes;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = {
|
|
196
|
+
insertMessage,
|
|
197
|
+
getMessageById,
|
|
198
|
+
getAllPending,
|
|
199
|
+
markDelivered,
|
|
200
|
+
markInTransit,
|
|
201
|
+
incrementHop,
|
|
202
|
+
updateMessagePayload,
|
|
203
|
+
getMessagesForDestination,
|
|
204
|
+
upsertMessage,
|
|
205
|
+
upsertPeer,
|
|
206
|
+
getPeerByIdentity,
|
|
207
|
+
getPeerPublicKey,
|
|
208
|
+
getActivePeers,
|
|
209
|
+
getAllPeers,
|
|
210
|
+
markPeerOffline,
|
|
211
|
+
clearAllPeerIPs,
|
|
212
|
+
removeInactivePeers,
|
|
213
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
|
|
9
|
+
const IB_DIR = path.join(os.homedir(), '.ib'); //if any user has installed this library, its directory will get stored with this path
|
|
10
|
+
const IDENTITY_FILE = path.join(IB_DIR, 'identity.json');
|
|
11
|
+
|
|
12
|
+
function generateShortId() {
|
|
13
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
14
|
+
let id = '';
|
|
15
|
+
// Generate extra bytes to account for rejection sampling (uniform distribution)
|
|
16
|
+
const bytes = crypto.randomBytes(16); //generate 16 random numbers from 0 -> 255
|
|
17
|
+
let byteIndex = 0;
|
|
18
|
+
while (id.length < 4) {
|
|
19
|
+
const byte = bytes[byteIndex++];
|
|
20
|
+
// Only accept bytes that fall within chars length
|
|
21
|
+
if (byte < 256 - (256 % chars.length)) {
|
|
22
|
+
id += chars[byte % chars.length];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return id;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
function promptUsername() {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const rl = readline.createInterface({
|
|
32
|
+
input: process.stdin,
|
|
33
|
+
output: process.stdout,
|
|
34
|
+
});
|
|
35
|
+
rl.question('\nš Welcome to The Invisible Billion!\nEnter your username (e.g. shivam): ', (answer) => {
|
|
36
|
+
rl.close();
|
|
37
|
+
const name = answer.trim().toLowerCase().replace(/[^a-z0-9_-]/g, '');
|
|
38
|
+
if (!name) {
|
|
39
|
+
console.error('Username cannot be empty. Using "user" as default.');
|
|
40
|
+
resolve('user');
|
|
41
|
+
} else {
|
|
42
|
+
resolve(name);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Load existing identity from disk, or create a new one interactively.
|
|
50
|
+
* Returns: { username, shortId, identity }
|
|
51
|
+
* where identity = "username@shortId"
|
|
52
|
+
*/
|
|
53
|
+
async function loadOrCreate() {
|
|
54
|
+
// Ensure ~/.ib directory exists
|
|
55
|
+
if (!fs.existsSync(IB_DIR)) {
|
|
56
|
+
fs.mkdirSync(IB_DIR, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (fs.existsSync(IDENTITY_FILE)) {
|
|
60
|
+
const raw = fs.readFileSync(IDENTITY_FILE, 'utf8');
|
|
61
|
+
const data = JSON.parse(raw);
|
|
62
|
+
return data;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// First time prompt and create your username
|
|
66
|
+
const username = await promptUsername();
|
|
67
|
+
const shortId = generateShortId();
|
|
68
|
+
const identity = `${username}@${shortId}`;
|
|
69
|
+
|
|
70
|
+
const data = { username, shortId, identity };
|
|
71
|
+
fs.writeFileSync(IDENTITY_FILE, JSON.stringify(data, null, 2), 'utf8');
|
|
72
|
+
|
|
73
|
+
console.log(`\nā
Identity created: ${identity}`);
|
|
74
|
+
console.log(` Your IB ID is: ${identity}`);
|
|
75
|
+
console.log(` Share this with contacts so they can message you.\n`);
|
|
76
|
+
|
|
77
|
+
return data;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Load identity without prompting ā throws error if not found.
|
|
82
|
+
* Used by the daemon (which should never prompt).
|
|
83
|
+
*/
|
|
84
|
+
function loadIdentity() {
|
|
85
|
+
if (!fs.existsSync(IDENTITY_FILE)) {
|
|
86
|
+
throw new Error('No identity found. Run: ib start');
|
|
87
|
+
}
|
|
88
|
+
const raw = fs.readFileSync(IDENTITY_FILE, 'utf8');
|
|
89
|
+
return JSON.parse(raw);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { loadOrCreate, loadIdentity, IB_DIR };
|