@botmem/apple-bridge 0.35.25
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/dist/cli.d.ts +8 -0
- package/dist/cli.js +90 -0
- package/dist/cli.js.map +1 -0
- package/dist/contacts.d.ts +15 -0
- package/dist/contacts.js +136 -0
- package/dist/contacts.js.map +1 -0
- package/dist/crypto.d.ts +33 -0
- package/dist/crypto.js +76 -0
- package/dist/crypto.js.map +1 -0
- package/dist/db.d.ts +62 -0
- package/dist/db.js +270 -0
- package/dist/db.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/preflight.d.ts +12 -0
- package/dist/preflight.js +62 -0
- package/dist/preflight.js.map +1 -0
- package/dist/rpc-handler.d.ts +26 -0
- package/dist/rpc-handler.js +62 -0
- package/dist/rpc-handler.js.map +1 -0
- package/dist/tunnel.d.ts +38 -0
- package/dist/tunnel.js +203 -0
- package/dist/tunnel.js.map +1 -0
- package/package.json +40 -0
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Botmem Apple Bridge CLI.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx @botmem/apple-bridge --token=<token> [--server=wss://botmem.xyz/imsg-tunnel]
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import { runPreflight, DEFAULT_DB_PATH } from './preflight.js';
|
|
10
|
+
import { ImsgDatabase } from './db.js';
|
|
11
|
+
import { RpcHandler } from './rpc-handler.js';
|
|
12
|
+
import { TunnelClient } from './tunnel.js';
|
|
13
|
+
const DEFAULT_SERVER = 'wss://botmem.xyz/imsg-tunnel';
|
|
14
|
+
const program = new Command();
|
|
15
|
+
function timestamp() {
|
|
16
|
+
return new Date().toISOString();
|
|
17
|
+
}
|
|
18
|
+
function log(message = '') {
|
|
19
|
+
console.log(message ? ` ${timestamp()} ${message}` : '');
|
|
20
|
+
}
|
|
21
|
+
function error(message = '') {
|
|
22
|
+
console.error(message ? ` ${timestamp()} ${message}` : '');
|
|
23
|
+
}
|
|
24
|
+
program
|
|
25
|
+
.name('apple-bridge')
|
|
26
|
+
.description('Botmem Apple Bridge — syncs local Apple data securely')
|
|
27
|
+
.requiredOption('--token <token>', 'Bridge token from your Botmem dashboard')
|
|
28
|
+
.option('--server <url>', 'Botmem server URL', DEFAULT_SERVER)
|
|
29
|
+
.option('--db <path>', 'Path to chat.db', DEFAULT_DB_PATH)
|
|
30
|
+
.action(async (opts) => {
|
|
31
|
+
console.log('\n BOTMEM APPLE BRIDGE\n');
|
|
32
|
+
// ── Preflight ─────────────────────────────────────────────────────────
|
|
33
|
+
log('Checking prerequisites...');
|
|
34
|
+
const preflight = runPreflight(opts.db);
|
|
35
|
+
if (!preflight.ok) {
|
|
36
|
+
error('PREFLIGHT FAILED:');
|
|
37
|
+
for (const err of preflight.errors) {
|
|
38
|
+
for (const line of err.split('\n')) {
|
|
39
|
+
error(line);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
log(`iMessage database: ${preflight.dbPath}`);
|
|
45
|
+
log(`Chats found: ${preflight.chatCount ?? 'unknown'}`);
|
|
46
|
+
// ── Open database ─────────────────────────────────────────────────────
|
|
47
|
+
const db = new ImsgDatabase(opts.db);
|
|
48
|
+
const rpcHandler = new RpcHandler(db);
|
|
49
|
+
// ── Connect tunnel ────────────────────────────────────────────────────
|
|
50
|
+
console.log('');
|
|
51
|
+
log(`Connecting to ${opts.server}...`);
|
|
52
|
+
const tunnel = new TunnelClient({
|
|
53
|
+
serverUrl: opts.server,
|
|
54
|
+
token: opts.token,
|
|
55
|
+
rpcHandler,
|
|
56
|
+
});
|
|
57
|
+
tunnel.on('status', (status) => {
|
|
58
|
+
const icon = {
|
|
59
|
+
connecting: '...',
|
|
60
|
+
authenticating: '...',
|
|
61
|
+
connected: 'OK',
|
|
62
|
+
disconnected: '--',
|
|
63
|
+
error: '!!',
|
|
64
|
+
}[status] || '??';
|
|
65
|
+
log(`[${icon}] ${status}`);
|
|
66
|
+
});
|
|
67
|
+
tunnel.on('log', (msg) => {
|
|
68
|
+
log(msg);
|
|
69
|
+
});
|
|
70
|
+
tunnel.on('fatal', (msg) => {
|
|
71
|
+
console.error('');
|
|
72
|
+
error(`FATAL: ${msg}`);
|
|
73
|
+
console.error('');
|
|
74
|
+
db.close();
|
|
75
|
+
process.exit(1);
|
|
76
|
+
});
|
|
77
|
+
tunnel.connect();
|
|
78
|
+
// ── Graceful shutdown ─────────────────────────────────────────────────
|
|
79
|
+
const shutdown = () => {
|
|
80
|
+
console.log('');
|
|
81
|
+
log('Shutting down...');
|
|
82
|
+
tunnel.destroy();
|
|
83
|
+
db.close();
|
|
84
|
+
process.exit(0);
|
|
85
|
+
};
|
|
86
|
+
process.on('SIGINT', shutdown);
|
|
87
|
+
process.on('SIGTERM', shutdown);
|
|
88
|
+
});
|
|
89
|
+
program.parse();
|
|
90
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface AppleContact {
|
|
2
|
+
id: string;
|
|
3
|
+
displayName?: string;
|
|
4
|
+
givenName?: string;
|
|
5
|
+
familyName?: string;
|
|
6
|
+
middleName?: string;
|
|
7
|
+
nickname?: string;
|
|
8
|
+
organization?: string;
|
|
9
|
+
jobTitle?: string;
|
|
10
|
+
birthday?: string;
|
|
11
|
+
emails: string[];
|
|
12
|
+
phones: string[];
|
|
13
|
+
imageAvailable?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function listAppleContacts(): Promise<AppleContact[]>;
|
package/dist/contacts.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
const SWIFT_HELPER = String.raw `
|
|
6
|
+
import Contacts
|
|
7
|
+
import Foundation
|
|
8
|
+
|
|
9
|
+
struct ContactPayload: Codable {
|
|
10
|
+
let id: String
|
|
11
|
+
let displayName: String?
|
|
12
|
+
let givenName: String?
|
|
13
|
+
let familyName: String?
|
|
14
|
+
let middleName: String?
|
|
15
|
+
let nickname: String?
|
|
16
|
+
let organization: String?
|
|
17
|
+
let jobTitle: String?
|
|
18
|
+
let birthday: String?
|
|
19
|
+
let emails: [String]
|
|
20
|
+
let phones: [String]
|
|
21
|
+
let imageAvailable: Bool
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let store = CNContactStore()
|
|
25
|
+
let keys: [CNKeyDescriptor] = [
|
|
26
|
+
CNContactIdentifierKey as CNKeyDescriptor,
|
|
27
|
+
CNContactGivenNameKey as CNKeyDescriptor,
|
|
28
|
+
CNContactFamilyNameKey as CNKeyDescriptor,
|
|
29
|
+
CNContactMiddleNameKey as CNKeyDescriptor,
|
|
30
|
+
CNContactNicknameKey as CNKeyDescriptor,
|
|
31
|
+
CNContactOrganizationNameKey as CNKeyDescriptor,
|
|
32
|
+
CNContactJobTitleKey as CNKeyDescriptor,
|
|
33
|
+
CNContactBirthdayKey as CNKeyDescriptor,
|
|
34
|
+
CNContactEmailAddressesKey as CNKeyDescriptor,
|
|
35
|
+
CNContactPhoneNumbersKey as CNKeyDescriptor,
|
|
36
|
+
CNContactImageDataAvailableKey as CNKeyDescriptor
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
40
|
+
var granted = false
|
|
41
|
+
var authError: Error?
|
|
42
|
+
store.requestAccess(for: .contacts) { ok, error in
|
|
43
|
+
granted = ok
|
|
44
|
+
authError = error
|
|
45
|
+
semaphore.signal()
|
|
46
|
+
}
|
|
47
|
+
semaphore.wait()
|
|
48
|
+
|
|
49
|
+
if !granted {
|
|
50
|
+
let message = authError?.localizedDescription ?? "Contacts permission was denied"
|
|
51
|
+
FileHandle.standardError.write(Data(message.utf8))
|
|
52
|
+
exit(2)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func clean(_ value: String) -> String? {
|
|
56
|
+
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
57
|
+
return trimmed.isEmpty ? nil : trimmed
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func birthdayString(_ date: DateComponents?) -> String? {
|
|
61
|
+
guard let date else { return nil }
|
|
62
|
+
var parts: [String] = []
|
|
63
|
+
if let year = date.year { parts.append(String(format: "%04d", year)) }
|
|
64
|
+
if let month = date.month { parts.append(String(format: "%02d", month)) }
|
|
65
|
+
if let day = date.day { parts.append(String(format: "%02d", day)) }
|
|
66
|
+
return parts.isEmpty ? nil : parts.joined(separator: "-")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let request = CNContactFetchRequest(keysToFetch: keys)
|
|
70
|
+
var contacts: [ContactPayload] = []
|
|
71
|
+
|
|
72
|
+
try store.enumerateContacts(with: request) { contact, _ in
|
|
73
|
+
let names = [contact.givenName, contact.middleName, contact.familyName]
|
|
74
|
+
.compactMap(clean)
|
|
75
|
+
.joined(separator: " ")
|
|
76
|
+
let displayName = clean(names) ?? clean(contact.nickname) ?? clean(contact.organizationName)
|
|
77
|
+
contacts.append(ContactPayload(
|
|
78
|
+
id: contact.identifier,
|
|
79
|
+
displayName: displayName,
|
|
80
|
+
givenName: clean(contact.givenName),
|
|
81
|
+
familyName: clean(contact.familyName),
|
|
82
|
+
middleName: clean(contact.middleName),
|
|
83
|
+
nickname: clean(contact.nickname),
|
|
84
|
+
organization: clean(contact.organizationName),
|
|
85
|
+
jobTitle: clean(contact.jobTitle),
|
|
86
|
+
birthday: birthdayString(contact.birthday),
|
|
87
|
+
emails: contact.emailAddresses.compactMap { clean(String($0.value)) },
|
|
88
|
+
phones: contact.phoneNumbers.compactMap { clean($0.value.stringValue) },
|
|
89
|
+
imageAvailable: contact.imageDataAvailable
|
|
90
|
+
))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let data = try JSONEncoder().encode(contacts)
|
|
94
|
+
FileHandle.standardOutput.write(data)
|
|
95
|
+
`;
|
|
96
|
+
export async function listAppleContacts() {
|
|
97
|
+
const dir = await mkdir(join(tmpdir(), `botmem-apple-contacts-${process.pid}`), {
|
|
98
|
+
recursive: true,
|
|
99
|
+
}).then(() => join(tmpdir(), `botmem-apple-contacts-${process.pid}`));
|
|
100
|
+
const helperPath = join(dir, 'contacts-helper.swift');
|
|
101
|
+
await writeFile(helperPath, SWIFT_HELPER, 'utf8');
|
|
102
|
+
try {
|
|
103
|
+
const stdout = await runSwift(helperPath);
|
|
104
|
+
const parsed = JSON.parse(stdout);
|
|
105
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
await rm(dir, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function runSwift(helperPath) {
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
const child = spawn('xcrun', ['swift', helperPath], {
|
|
114
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
115
|
+
});
|
|
116
|
+
let stdout = '';
|
|
117
|
+
let stderr = '';
|
|
118
|
+
child.stdout.setEncoding('utf8');
|
|
119
|
+
child.stderr.setEncoding('utf8');
|
|
120
|
+
child.stdout.on('data', (chunk) => {
|
|
121
|
+
stdout += chunk;
|
|
122
|
+
});
|
|
123
|
+
child.stderr.on('data', (chunk) => {
|
|
124
|
+
stderr += chunk;
|
|
125
|
+
});
|
|
126
|
+
child.on('error', reject);
|
|
127
|
+
child.on('close', (code) => {
|
|
128
|
+
if (code === 0) {
|
|
129
|
+
resolve(stdout);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
reject(new Error(stderr.trim() || `Apple Contacts helper exited with code ${code}`));
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=contacts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contacts.js","sourceRoot":"","sources":["../src/contacts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAiBjC,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0F9B,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,yBAAyB,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE;QAC9E,SAAS,EAAE,IAAI;KAChB,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,yBAAyB,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACtE,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,uBAAuB,CAAC,CAAC;IACtD,MAAM,SAAS,CAAC,UAAU,EAAE,YAAY,EAAE,MAAM,CAAC,CAAC;IAClD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,UAAU,CAAC,CAAC;QAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAY,CAAC;QAC7C,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAE,MAAyB,CAAC,CAAC,CAAC,EAAE,CAAC;IACjE,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,UAAkB;IAClC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE;YAClD,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;SAClC,CAAC,CAAC;QACH,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QACjC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QACjC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;YAChC,MAAM,IAAI,KAAK,CAAC;QAClB,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;YAChC,MAAM,IAAI,KAAK,CAAC;QAClB,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC1B,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;YACzB,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;gBACf,OAAO,CAAC,MAAM,CAAC,CAAC;gBAChB,OAAO;YACT,CAAC;YACD,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,0CAA0C,IAAI,EAAE,CAAC,CAAC,CAAC;QACvF,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end encryption for the Apple bridge tunnel.
|
|
3
|
+
*
|
|
4
|
+
* Protocol:
|
|
5
|
+
* 1. Both sides generate ephemeral X25519 key pairs
|
|
6
|
+
* 2. Exchange public keys during auth handshake
|
|
7
|
+
* 3. Derive shared secret via ECDH
|
|
8
|
+
* 4. Derive AES-256-GCM key via HKDF-SHA256
|
|
9
|
+
* 5. Every frame encrypted with unique random IV
|
|
10
|
+
*
|
|
11
|
+
* Wire format (binary): [12-byte IV][ciphertext][16-byte auth tag]
|
|
12
|
+
*/
|
|
13
|
+
import { type KeyObject } from 'node:crypto';
|
|
14
|
+
export interface KeyPair {
|
|
15
|
+
publicKey: KeyObject;
|
|
16
|
+
privateKey: KeyObject;
|
|
17
|
+
}
|
|
18
|
+
/** Generate an ephemeral X25519 key pair for ECDH. */
|
|
19
|
+
export declare function generateKeyPair(): KeyPair;
|
|
20
|
+
/** Export public key to raw 32-byte Buffer for wire transfer. */
|
|
21
|
+
export declare function exportPublicKey(key: KeyObject): Buffer;
|
|
22
|
+
/** Import raw 32-byte public key from wire. */
|
|
23
|
+
export declare function importPublicKey(raw: Buffer): KeyObject;
|
|
24
|
+
/** Derive a shared AES-256 key from local private + remote public via ECDH + HKDF. */
|
|
25
|
+
export declare function deriveSessionKey(localPrivate: KeyObject, remotePublic: KeyObject): Buffer;
|
|
26
|
+
/** Encrypt plaintext with AES-256-GCM. Returns [IV (12) | ciphertext | tag (16)]. */
|
|
27
|
+
export declare function encrypt(key: Buffer, plaintext: string): Buffer;
|
|
28
|
+
/** Decrypt AES-256-GCM payload. Input: [IV (12) | ciphertext | tag (16)]. */
|
|
29
|
+
export declare function decrypt(key: Buffer, payload: Buffer): string;
|
|
30
|
+
/** Encrypt a JSON-serializable object. */
|
|
31
|
+
export declare function encryptJson(key: Buffer, data: unknown): Buffer;
|
|
32
|
+
/** Decrypt a payload and parse as JSON. */
|
|
33
|
+
export declare function decryptJson<T = unknown>(key: Buffer, payload: Buffer): T;
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end encryption for the Apple bridge tunnel.
|
|
3
|
+
*
|
|
4
|
+
* Protocol:
|
|
5
|
+
* 1. Both sides generate ephemeral X25519 key pairs
|
|
6
|
+
* 2. Exchange public keys during auth handshake
|
|
7
|
+
* 3. Derive shared secret via ECDH
|
|
8
|
+
* 4. Derive AES-256-GCM key via HKDF-SHA256
|
|
9
|
+
* 5. Every frame encrypted with unique random IV
|
|
10
|
+
*
|
|
11
|
+
* Wire format (binary): [12-byte IV][ciphertext][16-byte auth tag]
|
|
12
|
+
*/
|
|
13
|
+
import { generateKeyPairSync, diffieHellman, hkdfSync, randomBytes, createCipheriv, createDecipheriv, createPublicKey, } from 'node:crypto';
|
|
14
|
+
/** Generate an ephemeral X25519 key pair for ECDH. */
|
|
15
|
+
export function generateKeyPair() {
|
|
16
|
+
const { publicKey, privateKey } = generateKeyPairSync('x25519');
|
|
17
|
+
return { publicKey, privateKey };
|
|
18
|
+
}
|
|
19
|
+
/** Export public key to raw 32-byte Buffer for wire transfer. */
|
|
20
|
+
export function exportPublicKey(key) {
|
|
21
|
+
// DER format for X25519 public key: 12-byte header + 32-byte key
|
|
22
|
+
const der = key.export({ type: 'spki', format: 'der' });
|
|
23
|
+
return Buffer.from(der.subarray(12));
|
|
24
|
+
}
|
|
25
|
+
/** Import raw 32-byte public key from wire. */
|
|
26
|
+
export function importPublicKey(raw) {
|
|
27
|
+
// Wrap raw 32 bytes in X25519 SPKI DER header
|
|
28
|
+
const header = Buffer.from('302a300506032b656e032100', 'hex');
|
|
29
|
+
const der = Buffer.concat([header, raw]);
|
|
30
|
+
return createPublicKey({ key: der, format: 'der', type: 'spki' });
|
|
31
|
+
}
|
|
32
|
+
/** Derive a shared AES-256 key from local private + remote public via ECDH + HKDF. */
|
|
33
|
+
export function deriveSessionKey(localPrivate, remotePublic) {
|
|
34
|
+
const sharedSecret = diffieHellman({
|
|
35
|
+
privateKey: localPrivate,
|
|
36
|
+
publicKey: remotePublic,
|
|
37
|
+
});
|
|
38
|
+
const salt = Buffer.from('botmem-imsg-tunnel-v1', 'utf-8');
|
|
39
|
+
const info = Buffer.from('aes-256-gcm-session-key', 'utf-8');
|
|
40
|
+
const derived = hkdfSync('sha256', sharedSecret, salt, info, 32);
|
|
41
|
+
return Buffer.from(derived);
|
|
42
|
+
}
|
|
43
|
+
// ── Symmetric Encryption ────────────────────────────────────────────────────
|
|
44
|
+
const IV_LENGTH = 12;
|
|
45
|
+
const TAG_LENGTH = 16;
|
|
46
|
+
/** Encrypt plaintext with AES-256-GCM. Returns [IV (12) | ciphertext | tag (16)]. */
|
|
47
|
+
export function encrypt(key, plaintext) {
|
|
48
|
+
const iv = randomBytes(IV_LENGTH);
|
|
49
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
50
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
|
|
51
|
+
const tag = cipher.getAuthTag();
|
|
52
|
+
return Buffer.concat([iv, encrypted, tag]);
|
|
53
|
+
}
|
|
54
|
+
/** Decrypt AES-256-GCM payload. Input: [IV (12) | ciphertext | tag (16)]. */
|
|
55
|
+
export function decrypt(key, payload) {
|
|
56
|
+
if (payload.length < IV_LENGTH + TAG_LENGTH) {
|
|
57
|
+
throw new Error('Encrypted payload too short');
|
|
58
|
+
}
|
|
59
|
+
const iv = payload.subarray(0, IV_LENGTH);
|
|
60
|
+
const tag = payload.subarray(payload.length - TAG_LENGTH);
|
|
61
|
+
const ciphertext = payload.subarray(IV_LENGTH, payload.length - TAG_LENGTH);
|
|
62
|
+
const decipher = createDecipheriv('aes-256-gcm', key, iv);
|
|
63
|
+
decipher.setAuthTag(tag);
|
|
64
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
65
|
+
return decrypted.toString('utf-8');
|
|
66
|
+
}
|
|
67
|
+
// ── Convenience ─────────────────────────────────────────────────────────────
|
|
68
|
+
/** Encrypt a JSON-serializable object. */
|
|
69
|
+
export function encryptJson(key, data) {
|
|
70
|
+
return encrypt(key, JSON.stringify(data));
|
|
71
|
+
}
|
|
72
|
+
/** Decrypt a payload and parse as JSON. */
|
|
73
|
+
export function decryptJson(key, payload) {
|
|
74
|
+
return JSON.parse(decrypt(key, payload));
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"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"}
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite query layer for ~/Library/Messages/chat.db.
|
|
3
|
+
*
|
|
4
|
+
* Reads the iMessage database in read-only mode and returns data
|
|
5
|
+
* matching the JSON-RPC types expected by the Botmem iMessage connector.
|
|
6
|
+
*
|
|
7
|
+
* Core Data timestamp conversion:
|
|
8
|
+
* macOS stores dates as nanoseconds since 2001-01-01T00:00:00Z.
|
|
9
|
+
* Unix epoch offset: 978307200 seconds.
|
|
10
|
+
* Formula: new Date((date / 1e9 + 978307200) * 1000)
|
|
11
|
+
*/
|
|
12
|
+
export interface Chat {
|
|
13
|
+
id: number;
|
|
14
|
+
name: string;
|
|
15
|
+
identifier: string;
|
|
16
|
+
guid?: string;
|
|
17
|
+
service: string;
|
|
18
|
+
last_message_at: string;
|
|
19
|
+
participants?: string[];
|
|
20
|
+
is_group?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface Attachment {
|
|
23
|
+
filename?: string;
|
|
24
|
+
mime_type?: string;
|
|
25
|
+
transfer_name?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface Reaction {
|
|
28
|
+
sender?: string;
|
|
29
|
+
type?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface Message {
|
|
32
|
+
id: number;
|
|
33
|
+
chat_id: number;
|
|
34
|
+
guid: string;
|
|
35
|
+
sender: string;
|
|
36
|
+
is_from_me: boolean;
|
|
37
|
+
text: string;
|
|
38
|
+
created_at: string;
|
|
39
|
+
attachments: Attachment[];
|
|
40
|
+
reactions: Reaction[];
|
|
41
|
+
chat_identifier: string;
|
|
42
|
+
chat_name: string;
|
|
43
|
+
participants: string[];
|
|
44
|
+
is_group: boolean;
|
|
45
|
+
reply_to_guid?: string;
|
|
46
|
+
}
|
|
47
|
+
export declare class ImsgDatabase {
|
|
48
|
+
private db;
|
|
49
|
+
constructor(dbPath: string);
|
|
50
|
+
close(): void;
|
|
51
|
+
/** List chats sorted by most recent message. */
|
|
52
|
+
chatsList(limit?: number): Chat[];
|
|
53
|
+
/** Get message history for a chat with optional time-based pagination. */
|
|
54
|
+
messagesHistory(chatId: number, opts?: {
|
|
55
|
+
limit?: number;
|
|
56
|
+
start?: string;
|
|
57
|
+
end?: string;
|
|
58
|
+
}): Message[];
|
|
59
|
+
private getChatParticipants;
|
|
60
|
+
private getChatMeta;
|
|
61
|
+
private getAttachmentsForMessages;
|
|
62
|
+
}
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite query layer for ~/Library/Messages/chat.db.
|
|
3
|
+
*
|
|
4
|
+
* Reads the iMessage database in read-only mode and returns data
|
|
5
|
+
* matching the JSON-RPC types expected by the Botmem iMessage connector.
|
|
6
|
+
*
|
|
7
|
+
* Core Data timestamp conversion:
|
|
8
|
+
* macOS stores dates as nanoseconds since 2001-01-01T00:00:00Z.
|
|
9
|
+
* Unix epoch offset: 978307200 seconds.
|
|
10
|
+
* Formula: new Date((date / 1e9 + 978307200) * 1000)
|
|
11
|
+
*/
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
+
const Database = require('better-sqlite3');
|
|
16
|
+
// ── Constants ───────────────────────────────────────────────────────────────
|
|
17
|
+
/** Seconds between 2001-01-01 and 1970-01-01 (Unix epoch). */
|
|
18
|
+
const CORE_DATA_EPOCH_OFFSET = 978307200;
|
|
19
|
+
/** Convert Core Data nanosecond timestamp to ISO 8601 string. */
|
|
20
|
+
function coreDataToISO(nanos) {
|
|
21
|
+
if (!nanos || nanos === 0)
|
|
22
|
+
return new Date(0).toISOString();
|
|
23
|
+
const unixSeconds = nanos / 1_000_000_000 + CORE_DATA_EPOCH_OFFSET;
|
|
24
|
+
return new Date(unixSeconds * 1000).toISOString();
|
|
25
|
+
}
|
|
26
|
+
// ── Database ────────────────────────────────────────────────────────────────
|
|
27
|
+
export class ImsgDatabase {
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
db;
|
|
30
|
+
constructor(dbPath) {
|
|
31
|
+
this.db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
32
|
+
// WAL mode for concurrent reads while Messages.app writes
|
|
33
|
+
this.db.pragma('journal_mode = WAL');
|
|
34
|
+
}
|
|
35
|
+
close() {
|
|
36
|
+
this.db.close();
|
|
37
|
+
}
|
|
38
|
+
/** List chats sorted by most recent message. */
|
|
39
|
+
chatsList(limit) {
|
|
40
|
+
const sql = `
|
|
41
|
+
SELECT
|
|
42
|
+
c.ROWID as id,
|
|
43
|
+
COALESCE(c.display_name, '') as name,
|
|
44
|
+
c.guid as identifier,
|
|
45
|
+
c.guid,
|
|
46
|
+
COALESCE(c.service_name, 'iMessage') as service,
|
|
47
|
+
MAX(m.date) as last_message_date
|
|
48
|
+
FROM chat c
|
|
49
|
+
LEFT JOIN chat_message_join cmj ON cmj.chat_id = c.ROWID
|
|
50
|
+
LEFT JOIN message m ON m.ROWID = cmj.message_id
|
|
51
|
+
GROUP BY c.ROWID
|
|
52
|
+
ORDER BY last_message_date DESC
|
|
53
|
+
${limit ? 'LIMIT ?' : ''}
|
|
54
|
+
`;
|
|
55
|
+
const rows = limit ? this.db.prepare(sql).all(limit) : this.db.prepare(sql).all();
|
|
56
|
+
return rows.map((row) => {
|
|
57
|
+
const chatId = row.id;
|
|
58
|
+
const participants = this.getChatParticipants(chatId);
|
|
59
|
+
const isGroup = participants.length > 1;
|
|
60
|
+
return {
|
|
61
|
+
id: chatId,
|
|
62
|
+
name: row.name || (isGroup ? 'Group Chat' : participants[0] || 'Unknown'),
|
|
63
|
+
identifier: row.identifier,
|
|
64
|
+
guid: row.guid,
|
|
65
|
+
service: row.service,
|
|
66
|
+
last_message_at: coreDataToISO(row.last_message_date),
|
|
67
|
+
participants,
|
|
68
|
+
is_group: isGroup,
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Get message history for a chat with optional time-based pagination. */
|
|
73
|
+
messagesHistory(chatId, opts) {
|
|
74
|
+
// Get chat metadata once
|
|
75
|
+
const chatMeta = this.getChatMeta(chatId);
|
|
76
|
+
let sql = `
|
|
77
|
+
SELECT
|
|
78
|
+
m.ROWID as id,
|
|
79
|
+
m.guid,
|
|
80
|
+
m.text,
|
|
81
|
+
m.attributedBody,
|
|
82
|
+
m.date,
|
|
83
|
+
m.is_from_me,
|
|
84
|
+
m.cache_roomnames,
|
|
85
|
+
m.associated_message_guid,
|
|
86
|
+
m.associated_message_type,
|
|
87
|
+
h.id as handle_id
|
|
88
|
+
FROM message m
|
|
89
|
+
JOIN chat_message_join cmj ON cmj.message_id = m.ROWID
|
|
90
|
+
LEFT JOIN handle h ON h.ROWID = m.handle_id
|
|
91
|
+
WHERE cmj.chat_id = ?
|
|
92
|
+
`;
|
|
93
|
+
const params = [chatId];
|
|
94
|
+
if (opts?.start) {
|
|
95
|
+
const startNanos = isoToCoreData(opts.start);
|
|
96
|
+
sql += ' AND m.date >= ?';
|
|
97
|
+
params.push(startNanos);
|
|
98
|
+
}
|
|
99
|
+
if (opts?.end) {
|
|
100
|
+
const endNanos = isoToCoreData(opts.end);
|
|
101
|
+
sql += ' AND m.date <= ?';
|
|
102
|
+
params.push(endNanos);
|
|
103
|
+
}
|
|
104
|
+
sql += ' ORDER BY m.date ASC';
|
|
105
|
+
if (opts?.limit) {
|
|
106
|
+
sql += ' LIMIT ?';
|
|
107
|
+
params.push(opts.limit);
|
|
108
|
+
}
|
|
109
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
110
|
+
const participants = this.getChatParticipants(chatId);
|
|
111
|
+
const messageIds = rows.map((row) => row.id);
|
|
112
|
+
const attachmentsByMessageId = this.getAttachmentsForMessages(messageIds);
|
|
113
|
+
return rows.map((row) => {
|
|
114
|
+
const msgId = row.id;
|
|
115
|
+
const attachments = attachmentsByMessageId.get(msgId) || [];
|
|
116
|
+
const text = row.text ||
|
|
117
|
+
extractAttributedBodyText(row.attributedBody) ||
|
|
118
|
+
'';
|
|
119
|
+
return {
|
|
120
|
+
id: msgId,
|
|
121
|
+
chat_id: chatId,
|
|
122
|
+
guid: row.guid || `imsg-local-${msgId}`,
|
|
123
|
+
sender: row.handle_id || '',
|
|
124
|
+
is_from_me: row.is_from_me === 1,
|
|
125
|
+
text,
|
|
126
|
+
created_at: coreDataToISO(row.date),
|
|
127
|
+
attachments,
|
|
128
|
+
// Reactions are not used by the Botmem ingestion pipeline. Fetching them
|
|
129
|
+
// per message requires an unindexed suffix LIKE scan over the message
|
|
130
|
+
// table, which makes large chats time out.
|
|
131
|
+
reactions: [],
|
|
132
|
+
chat_identifier: chatMeta.identifier,
|
|
133
|
+
chat_name: chatMeta.name,
|
|
134
|
+
participants,
|
|
135
|
+
is_group: participants.length > 1,
|
|
136
|
+
reply_to_guid: row.associated_message_guid || undefined,
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// ── Private helpers ─────────────────────────────────────────────────────
|
|
141
|
+
getChatParticipants(chatId) {
|
|
142
|
+
const sql = `
|
|
143
|
+
SELECT h.id
|
|
144
|
+
FROM chat_handle_join chj
|
|
145
|
+
JOIN handle h ON h.ROWID = chj.handle_id
|
|
146
|
+
WHERE chj.chat_id = ?
|
|
147
|
+
`;
|
|
148
|
+
const rows = this.db.prepare(sql).all(chatId);
|
|
149
|
+
return rows.map((r) => r.id);
|
|
150
|
+
}
|
|
151
|
+
getChatMeta(chatId) {
|
|
152
|
+
const sql = `
|
|
153
|
+
SELECT COALESCE(display_name, '') as name, guid as identifier
|
|
154
|
+
FROM chat WHERE ROWID = ?
|
|
155
|
+
`;
|
|
156
|
+
const row = this.db.prepare(sql).get(chatId);
|
|
157
|
+
return row || { name: 'Unknown', identifier: '' };
|
|
158
|
+
}
|
|
159
|
+
getAttachmentsForMessages(messageIds) {
|
|
160
|
+
const byMessageId = new Map();
|
|
161
|
+
if (messageIds.length === 0)
|
|
162
|
+
return byMessageId;
|
|
163
|
+
const chunkSize = 900;
|
|
164
|
+
for (let i = 0; i < messageIds.length; i += chunkSize) {
|
|
165
|
+
const chunk = messageIds.slice(i, i + chunkSize);
|
|
166
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
167
|
+
const sql = `
|
|
168
|
+
SELECT
|
|
169
|
+
maj.message_id,
|
|
170
|
+
a.filename,
|
|
171
|
+
a.mime_type,
|
|
172
|
+
a.transfer_name
|
|
173
|
+
FROM message_attachment_join maj
|
|
174
|
+
JOIN attachment a ON a.ROWID = maj.attachment_id
|
|
175
|
+
WHERE maj.message_id IN (${placeholders})
|
|
176
|
+
`;
|
|
177
|
+
const rows = this.db.prepare(sql).all(...chunk);
|
|
178
|
+
for (const row of rows) {
|
|
179
|
+
const messageId = row.message_id;
|
|
180
|
+
const attachments = byMessageId.get(messageId) || [];
|
|
181
|
+
attachments.push({
|
|
182
|
+
filename: row.filename || undefined,
|
|
183
|
+
mime_type: row.mime_type || undefined,
|
|
184
|
+
transfer_name: row.transfer_name || undefined,
|
|
185
|
+
});
|
|
186
|
+
byMessageId.set(messageId, attachments);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return byMessageId;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
193
|
+
function isoToCoreData(iso) {
|
|
194
|
+
const unixMs = new Date(iso).getTime();
|
|
195
|
+
const unixSeconds = unixMs / 1000;
|
|
196
|
+
return (unixSeconds - CORE_DATA_EPOCH_OFFSET) * 1_000_000_000;
|
|
197
|
+
}
|
|
198
|
+
function extractAttributedBodyText(body) {
|
|
199
|
+
if (!body?.length)
|
|
200
|
+
return '';
|
|
201
|
+
const nsString = Buffer.from('NSString', 'utf8');
|
|
202
|
+
const marker = Buffer.from([0x95, 0x84, 0x01, 0x2b]);
|
|
203
|
+
let searchFrom = 0;
|
|
204
|
+
while (searchFrom < body.length) {
|
|
205
|
+
const stringClassAt = body.indexOf(nsString, searchFrom);
|
|
206
|
+
if (stringClassAt === -1)
|
|
207
|
+
return '';
|
|
208
|
+
const markerAt = body.indexOf(marker, stringClassAt + nsString.length);
|
|
209
|
+
if (markerAt === -1)
|
|
210
|
+
return '';
|
|
211
|
+
const lengthAt = markerAt + marker.length;
|
|
212
|
+
const parsed = readArchivedStringLength(body, lengthAt);
|
|
213
|
+
if (!parsed) {
|
|
214
|
+
searchFrom = stringClassAt + nsString.length;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const { length, offset } = parsed;
|
|
218
|
+
const start = lengthAt + offset;
|
|
219
|
+
const end = start + length;
|
|
220
|
+
if (length <= 0 || end > body.length) {
|
|
221
|
+
searchFrom = stringClassAt + nsString.length;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const text = body.subarray(start, end).toString('utf8').trim();
|
|
225
|
+
if (isLikelyMessageText(text))
|
|
226
|
+
return text;
|
|
227
|
+
searchFrom = stringClassAt + nsString.length;
|
|
228
|
+
}
|
|
229
|
+
return '';
|
|
230
|
+
}
|
|
231
|
+
function readArchivedStringLength(body, offset) {
|
|
232
|
+
const first = body[offset];
|
|
233
|
+
if (first === undefined)
|
|
234
|
+
return null;
|
|
235
|
+
if (first < 0x80)
|
|
236
|
+
return { length: first, offset: 1 };
|
|
237
|
+
const byteCount = first & 0x7f;
|
|
238
|
+
if (byteCount <= 0 || byteCount > 4 || offset + byteCount >= body.length)
|
|
239
|
+
return null;
|
|
240
|
+
let length = 0;
|
|
241
|
+
for (let i = 1; i <= byteCount; i++) {
|
|
242
|
+
length = (length << 8) + body[offset + i];
|
|
243
|
+
}
|
|
244
|
+
return { length, offset: 1 + byteCount };
|
|
245
|
+
}
|
|
246
|
+
function isLikelyMessageText(text) {
|
|
247
|
+
if (!text)
|
|
248
|
+
return false;
|
|
249
|
+
if (text.includes('\u0000'))
|
|
250
|
+
return false;
|
|
251
|
+
const blocked = new Set([
|
|
252
|
+
'NSString',
|
|
253
|
+
'NSMutableString',
|
|
254
|
+
'NSAttributedString',
|
|
255
|
+
'NSMutableAttributedString',
|
|
256
|
+
'NSObject',
|
|
257
|
+
'NSDictionary',
|
|
258
|
+
'NSNumber',
|
|
259
|
+
'NSValue',
|
|
260
|
+
'NSData',
|
|
261
|
+
'NSMutableData',
|
|
262
|
+
'NSKeyedArchiver',
|
|
263
|
+
]);
|
|
264
|
+
if (blocked.has(text))
|
|
265
|
+
return false;
|
|
266
|
+
if (text.startsWith('__kIM'))
|
|
267
|
+
return false;
|
|
268
|
+
return /[\p{L}\p{N}]/u.test(text);
|
|
269
|
+
}
|
|
270
|
+
//# sourceMappingURL=db.js.map
|
package/dist/db.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ImsgDatabase } from './db.js';
|
|
2
|
+
export type { Chat, Message, Attachment, Reaction } from './db.js';
|
|
3
|
+
export { RpcHandler } from './rpc-handler.js';
|
|
4
|
+
export { TunnelClient } from './tunnel.js';
|
|
5
|
+
export type { TunnelOptions, TunnelStatus } from './tunnel.js';
|
|
6
|
+
export { generateKeyPair, exportPublicKey, importPublicKey, deriveSessionKey, encrypt, decrypt, encryptJson, decryptJson, } from './crypto.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Public API for programmatic use
|
|
2
|
+
export { ImsgDatabase } from './db.js';
|
|
3
|
+
export { RpcHandler } from './rpc-handler.js';
|
|
4
|
+
export { TunnelClient } from './tunnel.js';
|
|
5
|
+
export { generateKeyPair, exportPublicKey, importPublicKey, deriveSessionKey, encrypt, decrypt, encryptJson, decryptJson, } from './crypto.js';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EACL,eAAe,EACf,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,OAAO,EACP,OAAO,EACP,WAAW,EACX,WAAW,GACZ,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-flight checks for the Apple bridge.
|
|
3
|
+
* Verifies: macOS, chat.db readable, SQLite works.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_DB_PATH: string;
|
|
6
|
+
export interface PreflightResult {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
dbPath: string;
|
|
9
|
+
chatCount?: number;
|
|
10
|
+
errors: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function runPreflight(dbPath?: string): PreflightResult;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-flight checks for the Apple bridge.
|
|
3
|
+
* Verifies: macOS, chat.db readable, SQLite works.
|
|
4
|
+
*/
|
|
5
|
+
import { accessSync, constants } from 'node:fs';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
export const DEFAULT_DB_PATH = join(homedir(), 'Library/Messages/chat.db');
|
|
11
|
+
export function runPreflight(dbPath = DEFAULT_DB_PATH) {
|
|
12
|
+
const errors = [];
|
|
13
|
+
// 1. macOS check
|
|
14
|
+
if (process.platform !== 'darwin') {
|
|
15
|
+
errors.push(`This tool only runs on macOS (detected: ${process.platform}).` +
|
|
16
|
+
'\niMessage data is only available on Apple devices.');
|
|
17
|
+
return { ok: false, dbPath, errors };
|
|
18
|
+
}
|
|
19
|
+
// 2. File exists and readable
|
|
20
|
+
try {
|
|
21
|
+
accessSync(dbPath, constants.R_OK);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
errors.push(`Cannot read ${dbPath}` +
|
|
25
|
+
'\n\nTo fix this, grant Full Disk Access to your terminal:' +
|
|
26
|
+
'\n 1. Open System Settings → Privacy & Security → Full Disk Access' +
|
|
27
|
+
'\n 2. Click the + button and add your terminal app (Terminal, iTerm2, etc.)' +
|
|
28
|
+
'\n 3. Restart your terminal and try again');
|
|
29
|
+
return { ok: false, dbPath, errors };
|
|
30
|
+
}
|
|
31
|
+
// 3. SQLite can open and query the DB
|
|
32
|
+
try {
|
|
33
|
+
// Dynamic import to avoid requiring better-sqlite3 at module level
|
|
34
|
+
const Database = require('better-sqlite3');
|
|
35
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
36
|
+
try {
|
|
37
|
+
const row = db.prepare('SELECT count(*) as cnt FROM chat').get();
|
|
38
|
+
db.close();
|
|
39
|
+
return { ok: true, dbPath, chatCount: row.cnt, errors: [] };
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
try {
|
|
43
|
+
db.close();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* already closed */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
52
|
+
if (msg.includes('SQLITE_CANTOPEN') || msg.includes('unable to open')) {
|
|
53
|
+
errors.push(`Cannot open ${dbPath} as SQLite database.` +
|
|
54
|
+
'\nThe file may be locked or corrupted. Try closing Messages.app and retrying.');
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
errors.push(`SQLite error: ${msg}`);
|
|
58
|
+
}
|
|
59
|
+
return { ok: false, dbPath, errors };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=preflight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preflight.js","sourceRoot":"","sources":["../src/preflight.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/C,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,0BAA0B,CAAC,CAAC;AAS3E,MAAM,UAAU,YAAY,CAAC,SAAiB,eAAe;IAC3D,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,iBAAiB;IACjB,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAClC,MAAM,CAAC,IAAI,CACT,2CAA2C,OAAO,CAAC,QAAQ,IAAI;YAC7D,qDAAqD,CACxD,CAAC;QACF,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACvC,CAAC;IAED,8BAA8B;IAC9B,IAAI,CAAC;QACH,UAAU,CAAC,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,CAAC,IAAI,CACT,eAAe,MAAM,EAAE;YACrB,2DAA2D;YAC3D,qEAAqE;YACrE,8EAA8E;YAC9E,4CAA4C,CAC/C,CAAC;QACF,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACvC,CAAC;IAED,sCAAsC;IACtC,IAAI,CAAC;QACH,mEAAmE;QAEnE,MAAM,QAAQ,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;QAC3C,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACzE,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,kCAAkC,CAAC,CAAC,GAAG,EAE7D,CAAC;YACF,EAAE,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;QAC9D,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC;gBACH,EAAE,CAAC,KAAK,EAAE,CAAC;YACb,CAAC;YAAC,MAAM,CAAC;gBACP,oBAAoB;YACtB,CAAC;QACH,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7D,IAAI,GAAG,CAAC,QAAQ,CAAC,iBAAiB,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC;YACtE,MAAM,CAAC,IAAI,CACT,eAAe,MAAM,sBAAsB;gBACzC,+EAA+E,CAClF,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,IAAI,CAAC,iBAAiB,GAAG,EAAE,CAAC,CAAC;QACtC,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACvC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC 2.0 request handler.
|
|
3
|
+
* Dispatches incoming requests to the SQLite query layer.
|
|
4
|
+
*/
|
|
5
|
+
import type { ImsgDatabase } from './db.js';
|
|
6
|
+
interface JsonRpcRequest {
|
|
7
|
+
jsonrpc: '2.0';
|
|
8
|
+
id: number;
|
|
9
|
+
method: string;
|
|
10
|
+
params?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
interface JsonRpcResponse {
|
|
13
|
+
jsonrpc: '2.0';
|
|
14
|
+
id: number;
|
|
15
|
+
result?: unknown;
|
|
16
|
+
error?: {
|
|
17
|
+
code: number;
|
|
18
|
+
message: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export declare class RpcHandler {
|
|
22
|
+
private db;
|
|
23
|
+
constructor(db: ImsgDatabase);
|
|
24
|
+
handle(request: JsonRpcRequest): Promise<JsonRpcResponse>;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC 2.0 request handler.
|
|
3
|
+
* Dispatches incoming requests to the SQLite query layer.
|
|
4
|
+
*/
|
|
5
|
+
import { listAppleContacts } from './contacts.js';
|
|
6
|
+
export class RpcHandler {
|
|
7
|
+
db;
|
|
8
|
+
constructor(db) {
|
|
9
|
+
this.db = db;
|
|
10
|
+
}
|
|
11
|
+
async handle(request) {
|
|
12
|
+
const { id, method, params } = request;
|
|
13
|
+
try {
|
|
14
|
+
switch (method) {
|
|
15
|
+
case 'chats.list': {
|
|
16
|
+
const limit = params?.limit;
|
|
17
|
+
const chats = this.db.chatsList(limit);
|
|
18
|
+
return { jsonrpc: '2.0', id, result: { chats } };
|
|
19
|
+
}
|
|
20
|
+
case 'messages.history': {
|
|
21
|
+
const chatId = params?.chat_id;
|
|
22
|
+
if (chatId === undefined || chatId === null) {
|
|
23
|
+
return {
|
|
24
|
+
jsonrpc: '2.0',
|
|
25
|
+
id,
|
|
26
|
+
error: { code: -32602, message: 'Missing required param: chat_id' },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const opts = {
|
|
30
|
+
limit: params?.limit,
|
|
31
|
+
start: params?.start,
|
|
32
|
+
end: params?.end,
|
|
33
|
+
};
|
|
34
|
+
const messages = this.db.messagesHistory(chatId, opts);
|
|
35
|
+
return { jsonrpc: '2.0', id, result: { messages } };
|
|
36
|
+
}
|
|
37
|
+
case 'contacts.list': {
|
|
38
|
+
const contacts = await listAppleContacts();
|
|
39
|
+
return { jsonrpc: '2.0', id, result: { contacts } };
|
|
40
|
+
}
|
|
41
|
+
case 'ping': {
|
|
42
|
+
return { jsonrpc: '2.0', id, result: { pong: true, ts: Date.now() } };
|
|
43
|
+
}
|
|
44
|
+
default:
|
|
45
|
+
return {
|
|
46
|
+
jsonrpc: '2.0',
|
|
47
|
+
id,
|
|
48
|
+
error: { code: -32601, message: `Method not found: ${method}` },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
54
|
+
return {
|
|
55
|
+
jsonrpc: '2.0',
|
|
56
|
+
id,
|
|
57
|
+
error: { code: -32000, message: msg },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=rpc-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc-handler.js","sourceRoot":"","sources":["../src/rpc-handler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAgBlD,MAAM,OAAO,UAAU;IACD;IAApB,YAAoB,EAAgB;QAAhB,OAAE,GAAF,EAAE,CAAc;IAAG,CAAC;IAExC,KAAK,CAAC,MAAM,CAAC,OAAuB;QAClC,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;QAEvC,IAAI,CAAC;YACH,QAAQ,MAAM,EAAE,CAAC;gBACf,KAAK,YAAY,CAAC,CAAC,CAAC;oBAClB,MAAM,KAAK,GAAG,MAAM,EAAE,KAA2B,CAAC;oBAClD,MAAM,KAAK,GAAG,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;oBACvC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC;gBACnD,CAAC;gBAED,KAAK,kBAAkB,CAAC,CAAC,CAAC;oBACxB,MAAM,MAAM,GAAG,MAAM,EAAE,OAAiB,CAAC;oBACzC,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;wBAC5C,OAAO;4BACL,OAAO,EAAE,KAAK;4BACd,EAAE;4BACF,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,iCAAiC,EAAE;yBACpE,CAAC;oBACJ,CAAC;oBACD,MAAM,IAAI,GAAG;wBACX,KAAK,EAAE,MAAM,EAAE,KAA2B;wBAC1C,KAAK,EAAE,MAAM,EAAE,KAA2B;wBAC1C,GAAG,EAAE,MAAM,EAAE,GAAyB;qBACvC,CAAC;oBACF,MAAM,QAAQ,GAAG,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;oBACvD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC;gBACtD,CAAC;gBAED,KAAK,eAAe,CAAC,CAAC,CAAC;oBACrB,MAAM,QAAQ,GAAG,MAAM,iBAAiB,EAAE,CAAC;oBAC3C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC;gBACtD,CAAC;gBAED,KAAK,MAAM,CAAC,CAAC,CAAC;oBACZ,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,EAAE,CAAC;gBACxE,CAAC;gBAED;oBACE,OAAO;wBACL,OAAO,EAAE,KAAK;wBACd,EAAE;wBACF,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,qBAAqB,MAAM,EAAE,EAAE;qBAChE,CAAC;YACN,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC7D,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,EAAE;gBACF,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE;aACtC,CAAC;QACJ,CAAC;IACH,CAAC;CACF"}
|
package/dist/tunnel.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket tunnel client.
|
|
3
|
+
*
|
|
4
|
+
* Connects to the Botmem server, performs ECDH key exchange,
|
|
5
|
+
* then relays encrypted JSON-RPC requests to the local RPC handler.
|
|
6
|
+
*/
|
|
7
|
+
import { EventEmitter } from 'node:events';
|
|
8
|
+
import type { RpcHandler } from './rpc-handler.js';
|
|
9
|
+
export interface TunnelOptions {
|
|
10
|
+
serverUrl: string;
|
|
11
|
+
token: string;
|
|
12
|
+
rpcHandler: RpcHandler;
|
|
13
|
+
}
|
|
14
|
+
export type TunnelStatus = 'connecting' | 'authenticating' | 'connected' | 'disconnected' | 'error';
|
|
15
|
+
export declare class TunnelClient extends EventEmitter {
|
|
16
|
+
private opts;
|
|
17
|
+
private ws;
|
|
18
|
+
private sessionKey;
|
|
19
|
+
private reconnectAttempt;
|
|
20
|
+
private reconnectTimer;
|
|
21
|
+
private heartbeatTimer;
|
|
22
|
+
private heartbeatTimeout;
|
|
23
|
+
private destroyed;
|
|
24
|
+
private _status;
|
|
25
|
+
constructor(opts: TunnelOptions);
|
|
26
|
+
get status(): TunnelStatus;
|
|
27
|
+
/** Start the tunnel connection. */
|
|
28
|
+
connect(): void;
|
|
29
|
+
/** Gracefully disconnect. */
|
|
30
|
+
destroy(): void;
|
|
31
|
+
private performHandshake;
|
|
32
|
+
private handleAuthResponse;
|
|
33
|
+
private handleEncryptedMessage;
|
|
34
|
+
private startHeartbeat;
|
|
35
|
+
private scheduleReconnect;
|
|
36
|
+
private cleanup;
|
|
37
|
+
private setStatus;
|
|
38
|
+
}
|
package/dist/tunnel.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket tunnel client.
|
|
3
|
+
*
|
|
4
|
+
* Connects to the Botmem server, performs ECDH key exchange,
|
|
5
|
+
* then relays encrypted JSON-RPC requests to the local RPC handler.
|
|
6
|
+
*/
|
|
7
|
+
import WebSocket from 'ws';
|
|
8
|
+
import { EventEmitter } from 'node:events';
|
|
9
|
+
import { generateKeyPair, exportPublicKey, importPublicKey, deriveSessionKey, encryptJson, decryptJson, } from './crypto.js';
|
|
10
|
+
const MAX_BACKOFF_MS = 30_000;
|
|
11
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
12
|
+
const HEARTBEAT_TIMEOUT_MS = 10_000;
|
|
13
|
+
export class TunnelClient extends EventEmitter {
|
|
14
|
+
opts;
|
|
15
|
+
ws = null;
|
|
16
|
+
sessionKey = null;
|
|
17
|
+
reconnectAttempt = 0;
|
|
18
|
+
reconnectTimer = null;
|
|
19
|
+
heartbeatTimer = null;
|
|
20
|
+
heartbeatTimeout = null;
|
|
21
|
+
destroyed = false;
|
|
22
|
+
_status = 'disconnected';
|
|
23
|
+
constructor(opts) {
|
|
24
|
+
super();
|
|
25
|
+
this.opts = opts;
|
|
26
|
+
}
|
|
27
|
+
get status() {
|
|
28
|
+
return this._status;
|
|
29
|
+
}
|
|
30
|
+
/** Start the tunnel connection. */
|
|
31
|
+
connect() {
|
|
32
|
+
if (this.destroyed)
|
|
33
|
+
return;
|
|
34
|
+
this.setStatus('connecting');
|
|
35
|
+
const ws = new WebSocket(this.opts.serverUrl, {
|
|
36
|
+
headers: { 'User-Agent': 'botmem-apple-bridge/0.1' },
|
|
37
|
+
});
|
|
38
|
+
this.ws = ws;
|
|
39
|
+
ws.on('open', () => {
|
|
40
|
+
this.setStatus('authenticating');
|
|
41
|
+
this.performHandshake(ws);
|
|
42
|
+
});
|
|
43
|
+
ws.on('message', (data, isBinary) => {
|
|
44
|
+
if (this._status === 'authenticating') {
|
|
45
|
+
// Auth response is JSON text
|
|
46
|
+
this.handleAuthResponse(ws, typeof data === 'string' ? data : data.toString('utf-8'));
|
|
47
|
+
}
|
|
48
|
+
else if (this._status === 'connected') {
|
|
49
|
+
// All post-auth messages are encrypted binary
|
|
50
|
+
if (isBinary || Buffer.isBuffer(data)) {
|
|
51
|
+
this.handleEncryptedMessage(ws, Buffer.isBuffer(data) ? data : Buffer.from(data));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
ws.on('pong', () => {
|
|
56
|
+
if (this.heartbeatTimeout) {
|
|
57
|
+
clearTimeout(this.heartbeatTimeout);
|
|
58
|
+
this.heartbeatTimeout = null;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
ws.on('close', (code, reason) => {
|
|
62
|
+
this.cleanup();
|
|
63
|
+
if (!this.destroyed) {
|
|
64
|
+
this.emit('log', `Disconnected (code=${code}, reason=${reason?.toString() || 'none'})`);
|
|
65
|
+
this.setStatus('disconnected');
|
|
66
|
+
this.scheduleReconnect();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
ws.on('error', (err) => {
|
|
70
|
+
this.emit('log', `WebSocket error: ${err.message}`);
|
|
71
|
+
// 'close' will fire after this
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** Gracefully disconnect. */
|
|
75
|
+
destroy() {
|
|
76
|
+
this.destroyed = true;
|
|
77
|
+
this.cleanup();
|
|
78
|
+
if (this.ws) {
|
|
79
|
+
this.ws.close(1000, 'Bridge shutting down');
|
|
80
|
+
this.ws = null;
|
|
81
|
+
}
|
|
82
|
+
if (this.reconnectTimer) {
|
|
83
|
+
clearTimeout(this.reconnectTimer);
|
|
84
|
+
this.reconnectTimer = null;
|
|
85
|
+
}
|
|
86
|
+
this.setStatus('disconnected');
|
|
87
|
+
}
|
|
88
|
+
// ── Handshake ───────────────────────────────────────────────────────────
|
|
89
|
+
performHandshake(ws) {
|
|
90
|
+
const keyPair = generateKeyPair();
|
|
91
|
+
const publicKeyRaw = exportPublicKey(keyPair.publicKey);
|
|
92
|
+
// Store private key for deriving session key after server responds
|
|
93
|
+
ws._ecdhPrivate =
|
|
94
|
+
keyPair.privateKey;
|
|
95
|
+
ws.send(JSON.stringify({
|
|
96
|
+
event: 'auth',
|
|
97
|
+
data: {
|
|
98
|
+
token: this.opts.token,
|
|
99
|
+
publicKey: publicKeyRaw.toString('base64'),
|
|
100
|
+
},
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
handleAuthResponse(ws, raw) {
|
|
104
|
+
try {
|
|
105
|
+
const msg = JSON.parse(raw);
|
|
106
|
+
if (msg.event !== 'auth')
|
|
107
|
+
return;
|
|
108
|
+
if (!msg.data.ok) {
|
|
109
|
+
const reason = msg.data.reason || 'unknown';
|
|
110
|
+
this.emit('log', `Auth failed: ${reason}`);
|
|
111
|
+
// Permanent auth failures — don't reconnect
|
|
112
|
+
this.destroyed = true;
|
|
113
|
+
this.setStatus('error');
|
|
114
|
+
this.emit('fatal', `Authentication failed: ${reason}. Check your bridge token.`);
|
|
115
|
+
ws.close(4401, 'Auth failed');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (!msg.data.publicKey || !ws._ecdhPrivate) {
|
|
119
|
+
this.emit('log', 'Auth response missing public key');
|
|
120
|
+
this.setStatus('error');
|
|
121
|
+
ws.close(4400, 'Missing public key');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Derive session key
|
|
125
|
+
const serverPubRaw = Buffer.from(msg.data.publicKey, 'base64');
|
|
126
|
+
const serverPub = importPublicKey(serverPubRaw);
|
|
127
|
+
this.sessionKey = deriveSessionKey(ws._ecdhPrivate, serverPub);
|
|
128
|
+
// Clean up private key from ws object
|
|
129
|
+
delete ws._ecdhPrivate;
|
|
130
|
+
this.reconnectAttempt = 0;
|
|
131
|
+
this.setStatus('connected');
|
|
132
|
+
this.startHeartbeat(ws);
|
|
133
|
+
this.emit('log', 'Tunnel connected — encrypted session established');
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
this.emit('log', `Auth response parse error: ${err instanceof Error ? err.message : err}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// ── Encrypted message handling ──────────────────────────────────────────
|
|
140
|
+
async handleEncryptedMessage(ws, encrypted) {
|
|
141
|
+
if (!this.sessionKey)
|
|
142
|
+
return;
|
|
143
|
+
try {
|
|
144
|
+
const request = decryptJson(this.sessionKey, encrypted);
|
|
145
|
+
// Dispatch to RPC handler
|
|
146
|
+
const startedAt = Date.now();
|
|
147
|
+
const response = await this.opts.rpcHandler.handle(request);
|
|
148
|
+
const elapsedMs = Date.now() - startedAt;
|
|
149
|
+
if (elapsedMs >= 1000) {
|
|
150
|
+
this.emit('log', `RPC ${request.method} (id=${request.id}) completed in ${elapsedMs}ms`);
|
|
151
|
+
}
|
|
152
|
+
// Encrypt and send response
|
|
153
|
+
const encryptedResponse = encryptJson(this.sessionKey, response);
|
|
154
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
155
|
+
ws.send(encryptedResponse);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
this.emit('log', `Failed to handle message: ${err instanceof Error ? err.message : err}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// ── Heartbeat ───────────────────────────────────────────────────────────
|
|
163
|
+
startHeartbeat(ws) {
|
|
164
|
+
this.heartbeatTimer = setInterval(() => {
|
|
165
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
166
|
+
return;
|
|
167
|
+
ws.ping();
|
|
168
|
+
this.heartbeatTimeout = setTimeout(() => {
|
|
169
|
+
this.emit('log', 'Heartbeat timeout — closing connection');
|
|
170
|
+
ws.terminate();
|
|
171
|
+
}, HEARTBEAT_TIMEOUT_MS);
|
|
172
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
173
|
+
}
|
|
174
|
+
// ── Reconnection ────────────────────────────────────────────────────────
|
|
175
|
+
scheduleReconnect() {
|
|
176
|
+
if (this.destroyed)
|
|
177
|
+
return;
|
|
178
|
+
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempt), MAX_BACKOFF_MS);
|
|
179
|
+
this.reconnectAttempt++;
|
|
180
|
+
this.emit('log', `Reconnecting in ${(delay / 1000).toFixed(1)}s (attempt ${this.reconnectAttempt})`);
|
|
181
|
+
this.reconnectTimer = setTimeout(() => {
|
|
182
|
+
this.reconnectTimer = null;
|
|
183
|
+
this.connect();
|
|
184
|
+
}, delay);
|
|
185
|
+
}
|
|
186
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
187
|
+
cleanup() {
|
|
188
|
+
this.sessionKey = null;
|
|
189
|
+
if (this.heartbeatTimer) {
|
|
190
|
+
clearInterval(this.heartbeatTimer);
|
|
191
|
+
this.heartbeatTimer = null;
|
|
192
|
+
}
|
|
193
|
+
if (this.heartbeatTimeout) {
|
|
194
|
+
clearTimeout(this.heartbeatTimeout);
|
|
195
|
+
this.heartbeatTimeout = null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
setStatus(status) {
|
|
199
|
+
this._status = status;
|
|
200
|
+
this.emit('status', status);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=tunnel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnel.js","sourceRoot":"","sources":["../src/tunnel.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@botmem/apple-bridge",
|
|
3
|
+
"version": "0.35.25",
|
|
4
|
+
"description": "Apple bridge for Botmem — reads local Contacts and iMessages through an encrypted WebSocket tunnel",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"apple-bridge": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"dev": "tsc --watch",
|
|
14
|
+
"clean": "rm -rf dist"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"better-sqlite3": "^12.9.0",
|
|
18
|
+
"commander": "^14.0.3",
|
|
19
|
+
"ws": "^8.20.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/better-sqlite3": "^7.6.12",
|
|
23
|
+
"@types/node": "^25.6.0",
|
|
24
|
+
"@types/ws": "^8.5.13",
|
|
25
|
+
"typescript": "^5.7.0"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20"
|
|
29
|
+
},
|
|
30
|
+
"os": [
|
|
31
|
+
"darwin"
|
|
32
|
+
],
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT"
|
|
40
|
+
}
|