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.
@@ -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 };