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,113 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const { runCommand, sendCommand } = require('../ipc');
5
+ const { encrypt } = require('../../crypto/encrypt');
6
+ const { loadIdentity } = require('../../identity/index');
7
+ const { loadOrCreateKeypair } = require('../../crypto/keys');
8
+
9
+ /**
10
+ * ib send <destination> "<message>"
11
+ *
12
+ * 1. Validate identity format
13
+ * 2. Ask daemon for recipient's public key via get-peer-key IPC
14
+ * 3a. Key found → encrypt with hybrid AES+RSA, send pre-encrypted payload
15
+ * 3b. Key not found → store as pending (daemon will encrypt when peer discovered)
16
+ * 4. Daemon stores first, then attempts live TCP delivery immediately
17
+ */
18
+ async function sendCommand_(destination, messageText) {
19
+ if (!destination || !messageText) {
20
+ console.error('Usage: ib send <identity> "<message>"');
21
+ console.error('Example: ib send raj@a3f2 "bhai notes bhej"');
22
+ process.exit(1);
23
+ }
24
+
25
+ // Validate identity format: username@xxxx (4 alphanumeric chars)
26
+ if (!/^[a-z0-9_-]+@[a-z0-9]{4}$/.test(destination)) {
27
+ console.error(`❌ Invalid identity format: "${destination}"`);
28
+ console.error(' Expected: username@xxxx (e.g. raj@a3f2)');
29
+ process.exit(1);
30
+ }
31
+
32
+ // Load sender identity
33
+ let senderIdentity;
34
+ try {
35
+ senderIdentity = loadIdentity().identity;
36
+ } catch {
37
+ console.error('❌ Not initialised. Run: ib start');
38
+ process.exit(1);
39
+ }
40
+
41
+ // ── 1. Try to get recipient public key from daemon's peer table ──────────
42
+ let encryptedPayload;
43
+ let recipientPublicKey = null;
44
+
45
+ // Sending to ourselves? Use our own public key.
46
+ if (destination === senderIdentity) {
47
+ const { publicKey } = loadOrCreateKeypair();
48
+ recipientPublicKey = publicKey;
49
+ } else {
50
+ // Ask the daemon (it has the peer table from UDP discovery)
51
+ try {
52
+ const res = await sendCommand({ type: 'get-peer-key', identity: destination });
53
+ if (res.ok && res.publicKey) {
54
+ recipientPublicKey = res.publicKey;
55
+ }
56
+ } catch {
57
+ // Daemon may be unreachable — handled below
58
+ console.error('❌ Daemon is not running. Start it with: ib start');
59
+ process.exit(1);
60
+ }
61
+ }
62
+
63
+ // ── 2. Encrypt the message ───────────────────────────────────────────────
64
+ if (recipientPublicKey) {
65
+ try {
66
+ const blob = encrypt(messageText, recipientPublicKey);
67
+ encryptedPayload = JSON.stringify(blob);
68
+ console.log(`🔐 Message encrypted for ${destination}`);
69
+ } catch (err) {
70
+ console.error('❌ Encryption failed:', err.message);
71
+ process.exit(1);
72
+ }
73
+ } else {
74
+ // Peer's public key not yet known — store with pending flag
75
+ // Daemon will encrypt + re-deliver once it discovers the peer via UDP
76
+ console.log(`⚠️ ${destination} not yet on network — storing for epidemic delivery.`);
77
+ encryptedPayload = JSON.stringify({
78
+ __pending_encryption: true,
79
+ __phase1_plaintext: messageText,
80
+ });
81
+ }
82
+
83
+ // ── 3. Send to daemon (store-first, then live delivery attempt) ──────────
84
+ const messageId = crypto.randomUUID();
85
+ const ttl = Date.now() + 7 * 24 * 60 * 60 * 1000; // 7 days
86
+
87
+ await runCommand(
88
+ {
89
+ type: 'send',
90
+ id: messageId,
91
+ from_identity: senderIdentity,
92
+ destination,
93
+ payload: encryptedPayload,
94
+ ttl,
95
+ },
96
+ (res) => {
97
+ if (!res.ok) {
98
+ console.error('❌ Failed to queue message:', res.error);
99
+ return;
100
+ }
101
+
102
+ const isDelivered = res.status === 'delivered';
103
+ console.log(`\n📤 Message ${isDelivered ? 'delivered!' : 'queued!'}`);
104
+ console.log(` To: ${destination}`);
105
+ console.log(` Message: "${messageText}"`);
106
+ console.log(` ID: ${messageId}`);
107
+ console.log(` Status: ${isDelivered ? '✅ Delivered' : '⏳ Undelivered (will deliver when peer connects)'}`);
108
+ console.log(`\n Track it: ib status ${messageId}\n`);
109
+ }
110
+ );
111
+ }
112
+
113
+ module.exports = { sendCommand: sendCommand_ };
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * ib start — Start the background daemon.
5
+ *
6
+ * Handles:
7
+ * 1. First-time identity + keypair setup (interactive)
8
+ * 2. Crash recovery (stale PID file detection)
9
+ * 3. Idempotent: if daemon already running, says so and exits
10
+ * 4. Spawns daemon detached with windowsHide: true
11
+ * 5. Waits up to 3 s for daemon IPC to become ready, then confirms
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const os = require('os');
19
+ const { spawn } = require('child_process');
20
+ const { loadOrCreate } = require('../../identity/index');
21
+ const { loadOrCreateKeypair } = require('../../crypto/keys');
22
+ const { isDaemonRunning } = require('../ipc');
23
+
24
+ const IB_DIR = path.join(os.homedir(), '.ib');
25
+ const PID_FILE = path.join(IB_DIR, 'daemon.pid');
26
+ const DAEMON_ENTRY = path.resolve(__dirname, '../../daemon/index.js');
27
+
28
+ /**
29
+ * Check if a process with the given PID is alive.
30
+ * Cross-platform: signal 0 on unix, tasklist on Windows.
31
+ */
32
+ function isProcessAlive(pid) {
33
+ try {
34
+ // kill(pid, 0) does not send a signal — just checks existence
35
+ process.kill(pid, 0);
36
+ return true;
37
+ } catch (err) {
38
+ // EPERM means process exists but we can't signal it — still alive
39
+ return err.code === 'EPERM';
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Remove a stale PID file if the process is not running.
45
+ * Returns true if a stale PID was cleaned up.
46
+ */
47
+ function cleanStalePid() {
48
+ if (!fs.existsSync(PID_FILE)) return false;
49
+
50
+ const raw = fs.readFileSync(PID_FILE, 'utf8').trim();
51
+ const pid = parseInt(raw, 10);
52
+
53
+ if (!pid || isNaN(pid)) {
54
+ fs.unlinkSync(PID_FILE);
55
+ return true;
56
+ }
57
+
58
+ if (!isProcessAlive(pid)) {
59
+ console.log(`⚠️ Found stale PID file (PID ${pid} is not running). Cleaning up...`);
60
+ fs.unlinkSync(PID_FILE);
61
+ return true;
62
+ }
63
+
64
+ return false; // Process is alive — daemon is running
65
+ }
66
+
67
+ /**
68
+ * Wait for IPC to become responsive. Polls every 200ms up to maxMs.
69
+ */
70
+ async function waitForDaemon(maxMs = 3000) {
71
+ const start = Date.now();
72
+ while (Date.now() - start < maxMs) {
73
+ if (await isDaemonRunning()) return true;
74
+ await new Promise((r) => setTimeout(r, 200));
75
+ }
76
+ return false;
77
+ }
78
+
79
+ async function startCommand() {
80
+ // ── 1. First-time setup (identity + keypair) ───────────────────────────────
81
+ // Must happen BEFORE daemon spawn because daemon reads identity from disk.
82
+ const identity = await loadOrCreate();
83
+ loadOrCreateKeypair(); // generate if not exists, silent if already there
84
+
85
+ // ── 2. Crash recovery ──────────────────────────────────────────────────────
86
+ const wasStale = cleanStalePid();
87
+
88
+ // ── 3. Check if already running ───────────────────────────────────────────
89
+ if (!wasStale && fs.existsSync(PID_FILE)) {
90
+ // PID file exists and process is alive → already running
91
+ if (await isDaemonRunning()) {
92
+ console.log(`✅ The Invisible Billion daemon is already running as ${identity.identity}`);
93
+ return;
94
+ }
95
+ // PID alive but IPC not responding — force clean and restart
96
+ console.log('⚠️ Daemon PID exists but IPC is unresponsive. Force-restarting...');
97
+ fs.unlinkSync(PID_FILE);
98
+ }
99
+
100
+ // ── 4. Spawn daemon ───────────────────────────────────────────────────────
101
+ const child = spawn(process.execPath, [DAEMON_ENTRY], {
102
+ detached: true,
103
+ stdio: 'ignore',
104
+ windowsHide: true, // Critical: no console window on Windows
105
+ });
106
+ child.unref(); // Allow CLI to exit immediately
107
+
108
+ console.log(`🌍 Starting The Invisible Billion daemon... (PID will be ${child.pid})`);
109
+
110
+ // ── 5. Wait for IPC ready ─────────────────────────────────────────────────
111
+ const ready = await waitForDaemon(4000);
112
+
113
+ if (ready) {
114
+ console.log(`\n✅ The Invisible Billion daemon is running!`);
115
+ console.log(` Your identity: ${identity.identity}`);
116
+ console.log(` Log: ~/.ib/daemon.log`);
117
+ console.log(` Stop: ib stop\n`);
118
+ } else {
119
+ console.error(
120
+ '\n⚠️ Daemon spawned but IPC is not responding yet.\n' +
121
+ ' It may still be starting up — check ~/.ib/daemon.log for details.'
122
+ );
123
+ }
124
+ }
125
+
126
+ module.exports = { startCommand };
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ const { runCommand } = require('../ipc');
4
+
5
+ async function statusCommand(messageId) {
6
+ if (!messageId) {
7
+ console.error('Usage: ib status <messageId>');
8
+ process.exit(1);
9
+ }
10
+
11
+ await runCommand({ type: 'status', messageId }, (res) => {
12
+ if (!res.ok) {
13
+ console.error(`❌ ${res.error}`);
14
+ return;
15
+ }
16
+
17
+ const icons = {
18
+ undelivered: '🕐',
19
+ intransit: '🚀',
20
+ delivered: '✅',
21
+ };
22
+
23
+ const labels = {
24
+ undelivered: 'Undelivered (waiting for a carrier to pass through)',
25
+ intransit: 'In Transit (virus is spreading through the network)',
26
+ delivered: 'Delivered (recipient received and decrypted)',
27
+ };
28
+
29
+ const s = res.status;
30
+ const created = new Date(res.created_at).toLocaleString();
31
+ const expires = new Date(res.ttl).toLocaleString();
32
+
33
+ console.log(`\n${icons[s] || '❓'} Message Status`);
34
+ console.log(` ID: ${res.messageId}`);
35
+ console.log(` To: ${res.destination}`);
36
+ console.log(` Status: ${labels[s] || s}`);
37
+ console.log(` Hops: ${res.hop_count}`);
38
+ console.log(` Created: ${created}`);
39
+ console.log(` Expires: ${expires}\n`);
40
+ });
41
+ }
42
+
43
+ module.exports = { statusCommand };
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ const { runCommand } = require('../ipc');
4
+
5
+ async function stopCommand() {
6
+ await runCommand({ type: 'stop' }, (res) => {
7
+ if (res.ok) {
8
+ console.log('⛔ The Invisible Billion daemon stopped.');
9
+ } else {
10
+ console.error('❌ Stop failed:', res.error);
11
+ }
12
+ });
13
+ }
14
+
15
+ module.exports = { stopCommand };
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ const { runCommand } = require('../ipc');
4
+
5
+ async function syncCommand() {
6
+ console.log('🔄 Triggering sync with all known peers...');
7
+ await runCommand({ type: 'sync' }, (res) => {
8
+ if (!res.ok) {
9
+ console.error('❌ Sync failed:', res.error);
10
+ return;
11
+ }
12
+
13
+ console.log(`\n🔄 Sync complete!\n`);
14
+ console.log(` Pending messages: ${res.pending}`);
15
+ console.log(` Deliverable now: ${res.deliverable} (peers with known IPs)`);
16
+ console.log(` Known peers: ${res.peers}`);
17
+
18
+ if (res.pending > 0 && res.deliverable === 0) {
19
+ console.log(`\n ⏳ Messages are queued but destination peers are not`);
20
+ console.log(` yet on this network. They will deliver automatically`);
21
+ console.log(` when those peers appear. (This is Epidemic Routing)\n`);
22
+ } else if (res.deliverable > 0) {
23
+ console.log(`\n 📤 Delivery in progress — check status with:`);
24
+ console.log(` ib status <messageId>\n`);
25
+ } else {
26
+ console.log(`\n ✅ No pending messages.\n`);
27
+ }
28
+ });
29
+ }
30
+
31
+ module.exports = { syncCommand };
package/src/cli/ipc.js ADDED
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * CLI → Daemon IPC client.
5
+ *
6
+ * Connects to the daemon's IPC socket (Unix socket on Linux/macOS,
7
+ * named pipe on Windows), sends a newline-delimited JSON command,
8
+ * waits for the JSON response, then resolves.
9
+ *
10
+ * Never hangs — times out after 5 seconds.
11
+ */
12
+
13
+ const net = require('net');
14
+ const path = require('path');
15
+ const os = require('os');
16
+
17
+ const IB_DIR = path.join(os.homedir(), '.ib');
18
+
19
+ function getIPCPath() {
20
+ if (process.platform === 'win32') {
21
+ return '\\\\.\\pipe\\ib-ipc'; //Windows uses named pipes.
22
+ }
23
+ return path.join(IB_DIR, 'daemon.sock'); //Unix socket for Linux/macOS.
24
+ }
25
+
26
+ const IPC_SOCKET = getIPCPath();
27
+ const TIMEOUT_MS = 5000;
28
+
29
+ /**
30
+ * Send a command object to the daemon and return the response.
31
+ *
32
+ * @param {object} command - e.g. { type: 'ping' } or { type: 'send', ... }
33
+ * @returns {Promise<object>} - The daemon's JSON response
34
+ */
35
+ function sendCommand(command) {
36
+ return new Promise((resolve, reject) => {
37
+ const socket = new net.Socket();
38
+ let buffer = '';
39
+ let timedOut = false;
40
+
41
+ const timer = setTimeout(() => {
42
+ timedOut = true;
43
+ socket.destroy();
44
+ reject(new Error('TIMEOUT'));
45
+ }, TIMEOUT_MS);
46
+
47
+ socket.connect(IPC_SOCKET, () => {
48
+ socket.write(JSON.stringify(command) + '\n');
49
+ });
50
+
51
+ socket.on('data', (chunk) => {
52
+ buffer += chunk.toString();
53
+ const lines = buffer.split('\n');
54
+ buffer = lines.pop();
55
+
56
+ for (const line of lines) {
57
+ if (!line.trim()) continue;
58
+ clearTimeout(timer);
59
+ try {
60
+ const response = JSON.parse(line);
61
+ socket.destroy();
62
+ resolve(response);
63
+ } catch {
64
+ socket.destroy();
65
+ reject(new Error('Invalid response from daemon'));
66
+ }
67
+ return;
68
+ }
69
+ });
70
+
71
+ socket.on('error', (err) => {
72
+ if (timedOut) return;
73
+ clearTimeout(timer);
74
+ socket.destroy();
75
+ if (err.code === 'ENOENT' || err.code === 'ECONNREFUSED') {
76
+ reject(new Error('DAEMON_NOT_RUNNING'));
77
+ } else {
78
+ reject(err);
79
+ }
80
+ });
81
+
82
+ socket.on('close', () => {
83
+ if (timedOut) return;
84
+ clearTimeout(timer);
85
+ // If we got here without resolving, resolve with empty
86
+ resolve({ ok: false, error: 'Connection closed unexpectedly' });
87
+ });
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Convenience wrapper: send a command and print the result.
93
+ * Handles "daemon not running" with a user-friendly message.
94
+ *
95
+ * @param {object} command
96
+ * @param {function} formatter - (response) => void — formats and prints the result
97
+ */
98
+ async function runCommand(command, formatter) {
99
+ try {
100
+ const response = await sendCommand(command);
101
+ formatter(response);
102
+ } catch (err) {
103
+ if (err.message === 'DAEMON_NOT_RUNNING') {
104
+ console.error('❌ Daemon is not running. Start it with: ib start');
105
+ } else if (err.message === 'TIMEOUT') {
106
+ console.error('❌ Daemon is not responding (timeout). Try: ib stop && ib start');
107
+ } else {
108
+ console.error('❌ IPC error:', err.message);
109
+ }
110
+ process.exit(1);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Check if daemon is alive (ping).
116
+ * Returns true if responding, false otherwise.
117
+ */
118
+ async function isDaemonRunning() {
119
+ try {
120
+ const res = await sendCommand({ type: 'ping' });
121
+ return res.ok === true;
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+
127
+ module.exports = { sendCommand, runCommand, isDaemonRunning, IPC_SOCKET };
@@ -0,0 +1,100 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Hybrid encryption: AES-256-GCM (for arbitrary-length plaintext)
5
+ * + RSA-OAEP (to encrypt the AES key).
6
+ */
7
+
8
+ const crypto = require('crypto');
9
+
10
+ /**
11
+ * RSA encryption needs something called padding.
12
+ * Padding is extra data added before encryption to make RSA secure.
13
+ */
14
+ const RSA_PADDING = crypto.constants.RSA_PKCS1_OAEP_PADDING;
15
+
16
+ /**
17
+ * OAEP is a padding scheme for RSA encryption.
18
+ * It uses a hash function (SHA-256) to add randomness and security.
19
+ */
20
+
21
+ const OAEP_HASH = 'sha256';
22
+
23
+ //The crypto functions expect binary data, not strings.
24
+
25
+ /**
26
+ * Encrypt a plaintext string for a recipient.
27
+ *
28
+ * @param {string} plaintext - The message to encrypt
29
+ * @param {string} recipientPublicKeyPem - Recipient's RSA public key (PEM)
30
+ * @returns {{ encryptedKey: string, iv: string, tag: string, ciphertext: string }}
31
+ */
32
+ function encrypt(plaintext, recipientPublicKeyPem) {
33
+
34
+ //Generate a fresh random AES-256 key for this message only
35
+ const aesKey = crypto.randomBytes(32); // 256-bit
36
+ const iv = crypto.randomBytes(12); // 96-bit IV for GCM
37
+
38
+ // Encrypt plaintext with AES-256-GCM
39
+ const cipher = crypto.createCipheriv('aes-256-gcm', aesKey, iv); // in decryption this process will happen secondly, then down one(Interms of decryption)
40
+ const ciphertextBuf = Buffer.concat([
41
+ cipher.update(plaintext, 'utf8'),
42
+ cipher.final(),
43
+ ]);
44
+ const tag = cipher.getAuthTag(); // while encrypting, we also generate a 16-byte authentication tag
45
+
46
+ // Encrypt the AES key with the recipient's RSA public key
47
+ const encryptedKey = crypto.publicEncrypt(
48
+ {
49
+ key: recipientPublicKeyPem, //hence this should also be binary-data?
50
+ padding: RSA_PADDING,
51
+ oaepHash: OAEP_HASH,
52
+ },
53
+ aesKey
54
+ );
55
+
56
+ return {
57
+ encryptedKey: encryptedKey.toString('hex'),
58
+ iv: iv.toString('hex'),//be careful with this syntax, dont cconfuse with the Buffer.from(string, 'hex')
59
+ tag: tag.toString('hex'),
60
+ ciphertext: ciphertextBuf.toString('hex'),
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Decrypt a payload produced by encrypt().
66
+ *
67
+ * @param {{ encryptedKey: string, iv: string, tag: string, ciphertext: string }} payload
68
+ * @param {string} privateKeyPem - Recipient's RSA private key (PEM)
69
+ * @returns {string} - Decrypted plaintext
70
+ */
71
+ function decrypt(payload, privateKeyPem) { //payload is the encrypted message
72
+ const { encryptedKey, iv, tag, ciphertext } = payload;
73
+
74
+ //unlocking rsa stuff
75
+ const aesKey = crypto.privateDecrypt(
76
+ {
77
+ key: privateKeyPem,
78
+ padding: RSA_PADDING,
79
+ oaepHash: OAEP_HASH,
80
+ },
81
+ Buffer.from(encryptedKey, 'hex')
82
+ );
83
+
84
+ // creating a decryption engine
85
+ const decipher = crypto.createDecipheriv(
86
+ 'aes-256-gcm',
87
+ aesKey,
88
+ Buffer.from(iv, 'hex')
89
+ );
90
+ decipher.setAuthTag(Buffer.from(tag, 'hex'));
91
+
92
+ const plaintext = Buffer.concat([
93
+ decipher.update(Buffer.from(ciphertext, 'hex')),
94
+ decipher.final(),
95
+ ]);
96
+
97
+ return plaintext.toString('utf8');
98
+ }
99
+
100
+ module.exports = { encrypt, decrypt };
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const { IB_DIR } = require('../identity/index');
7
+
8
+ const KEYS_DIR = path.join(IB_DIR, 'keys');
9
+ const PUBLIC_KEY_FILE = path.join(KEYS_DIR, 'public.pem');
10
+ const PRIVATE_KEY_FILE = path.join(KEYS_DIR, 'private.pem');
11
+
12
+ //using RSA algorithm to generate keypair
13
+ function generateKeypair() {
14
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
15
+ modulusLength: 2048,
16
+ //use spki format for public key and pkcs8 format for private key
17
+ //pem is a text format for keys
18
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
19
+ /**
20
+ * something like this
21
+ * -----BEGIN PUBLIC KEY-----
22
+ * MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
23
+ * -----END PUBLIC KEY-----
24
+ */
25
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
26
+ });
27
+ return { publicKey, privateKey };
28
+ }
29
+
30
+ /**
31
+ * Load existing keypair from disk, or generate and save a new one.
32
+ * Returns { publicKey, privateKey } as PEM strings.
33
+ */
34
+ function loadOrCreateKeypair() {
35
+ if (!fs.existsSync(KEYS_DIR)) {
36
+ fs.mkdirSync(KEYS_DIR, { recursive: true });
37
+ }
38
+
39
+ //if key already exists, then load it
40
+ if (fs.existsSync(PUBLIC_KEY_FILE) && fs.existsSync(PRIVATE_KEY_FILE)) {
41
+ const publicKey = fs.readFileSync(PUBLIC_KEY_FILE, 'utf8');
42
+ const privateKey = fs.readFileSync(PRIVATE_KEY_FILE, 'utf8');
43
+ return { publicKey, privateKey };
44
+ }
45
+
46
+ //if not, generate and save it
47
+ const { publicKey, privateKey } = generateKeypair();
48
+ fs.writeFileSync(PUBLIC_KEY_FILE, publicKey, 'utf8');
49
+ // Private key: restrict permissions on unix-like systems
50
+ /*
51
+ The number is in octal format.
52
+
53
+ 0o600
54
+
55
+ Break it down:
56
+
57
+ Digit Meaning
58
+ 6 owner permissions
59
+ 0 group permissions
60
+ 0 others permissions
61
+ */
62
+ fs.writeFileSync(PRIVATE_KEY_FILE, privateKey, { encoding: 'utf8', mode: 0o600 });
63
+
64
+ console.log('🔑 RSA keypair generated and stored in ~/.ib/keys/');
65
+ return { publicKey, privateKey };
66
+ }
67
+
68
+ /**
69
+ * Load public key PEM for the local user.
70
+ * Throws if not found (daemon should call loadOrCreateKeypair first).
71
+ */
72
+ function loadPublicKey() {
73
+ if (!fs.existsSync(PUBLIC_KEY_FILE)) {
74
+ throw new Error('No public key found. Run: ib start');
75
+ }
76
+ return fs.readFileSync(PUBLIC_KEY_FILE, 'utf8');
77
+ }
78
+
79
+ /**
80
+ * Load private key PEM for the local user, also used by daemon to sign messages
81
+ */
82
+ function loadPrivateKey() {
83
+ if (!fs.existsSync(PRIVATE_KEY_FILE)) {
84
+ throw new Error('No private key found. Run: ib start');
85
+ }
86
+ return fs.readFileSync(PRIVATE_KEY_FILE, 'utf8');
87
+ }
88
+
89
+ module.exports = {
90
+ loadOrCreateKeypair,
91
+ loadPublicKey,
92
+ loadPrivateKey,
93
+ PUBLIC_KEY_FILE,
94
+ PRIVATE_KEY_FILE,
95
+ };