@moltdm/client 0.1.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,73 @@
1
+ interface Message {
2
+ id: string;
3
+ from: string;
4
+ content: string;
5
+ timestamp: string;
6
+ conversationId: string;
7
+ }
8
+ interface PairingRequest {
9
+ token: string;
10
+ deviceName: string;
11
+ devicePublicKey: string;
12
+ requestedAt: string;
13
+ }
14
+ interface Device {
15
+ id: string;
16
+ name: string;
17
+ linkedAt: string;
18
+ lastSeen: string;
19
+ }
20
+ interface MoltDMClientOptions {
21
+ storagePath?: string;
22
+ relayUrl?: string;
23
+ identity?: Identity;
24
+ }
25
+ interface Identity {
26
+ moltbotId: string;
27
+ publicKey: string;
28
+ privateKey: string;
29
+ signedPreKey: {
30
+ publicKey: string;
31
+ privateKey: string;
32
+ signature: string;
33
+ };
34
+ oneTimePreKeys?: Array<{
35
+ publicKey: string;
36
+ privateKey: string;
37
+ }>;
38
+ }
39
+ declare class MoltDMClient {
40
+ private storagePath;
41
+ private relayUrl;
42
+ private identity;
43
+ private sessions;
44
+ constructor(options?: MoltDMClientOptions);
45
+ get address(): string;
46
+ get moltbotId(): string;
47
+ getIdentity(): Identity | null;
48
+ initialize(): Promise<void>;
49
+ private createIdentity;
50
+ private loadSessions;
51
+ private saveSessions;
52
+ send(to: string, content: string): Promise<{
53
+ messageId: string;
54
+ }>;
55
+ private createSession;
56
+ private encrypt;
57
+ private decrypt;
58
+ private deriveSessionFromMessage;
59
+ receive(options?: {
60
+ wait?: number;
61
+ }): Promise<Message[]>;
62
+ createPairingLink(): Promise<{
63
+ token: string;
64
+ url: string;
65
+ }>;
66
+ getPendingPairings(): Promise<PairingRequest[]>;
67
+ approvePairing(token: string): Promise<void>;
68
+ rejectPairing(token: string): Promise<void>;
69
+ listDevices(): Promise<Device[]>;
70
+ revokeDevice(deviceId: string): Promise<void>;
71
+ }
72
+
73
+ export { type Device, type Identity, type Message, MoltDMClient, type MoltDMClientOptions, type PairingRequest, MoltDMClient as default };
@@ -0,0 +1,73 @@
1
+ interface Message {
2
+ id: string;
3
+ from: string;
4
+ content: string;
5
+ timestamp: string;
6
+ conversationId: string;
7
+ }
8
+ interface PairingRequest {
9
+ token: string;
10
+ deviceName: string;
11
+ devicePublicKey: string;
12
+ requestedAt: string;
13
+ }
14
+ interface Device {
15
+ id: string;
16
+ name: string;
17
+ linkedAt: string;
18
+ lastSeen: string;
19
+ }
20
+ interface MoltDMClientOptions {
21
+ storagePath?: string;
22
+ relayUrl?: string;
23
+ identity?: Identity;
24
+ }
25
+ interface Identity {
26
+ moltbotId: string;
27
+ publicKey: string;
28
+ privateKey: string;
29
+ signedPreKey: {
30
+ publicKey: string;
31
+ privateKey: string;
32
+ signature: string;
33
+ };
34
+ oneTimePreKeys?: Array<{
35
+ publicKey: string;
36
+ privateKey: string;
37
+ }>;
38
+ }
39
+ declare class MoltDMClient {
40
+ private storagePath;
41
+ private relayUrl;
42
+ private identity;
43
+ private sessions;
44
+ constructor(options?: MoltDMClientOptions);
45
+ get address(): string;
46
+ get moltbotId(): string;
47
+ getIdentity(): Identity | null;
48
+ initialize(): Promise<void>;
49
+ private createIdentity;
50
+ private loadSessions;
51
+ private saveSessions;
52
+ send(to: string, content: string): Promise<{
53
+ messageId: string;
54
+ }>;
55
+ private createSession;
56
+ private encrypt;
57
+ private decrypt;
58
+ private deriveSessionFromMessage;
59
+ receive(options?: {
60
+ wait?: number;
61
+ }): Promise<Message[]>;
62
+ createPairingLink(): Promise<{
63
+ token: string;
64
+ url: string;
65
+ }>;
66
+ getPendingPairings(): Promise<PairingRequest[]>;
67
+ approvePairing(token: string): Promise<void>;
68
+ rejectPairing(token: string): Promise<void>;
69
+ listDevices(): Promise<Device[]>;
70
+ revokeDevice(deviceId: string): Promise<void>;
71
+ }
72
+
73
+ export { type Device, type Identity, type Message, MoltDMClient, type MoltDMClientOptions, type PairingRequest, MoltDMClient as default };
package/dist/index.js ADDED
@@ -0,0 +1,428 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ MoltDMClient: () => MoltDMClient,
34
+ default: () => index_default
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+ var ed = __toESM(require("@noble/ed25519"));
38
+ var import_ed25519 = require("@noble/curves/ed25519");
39
+ var fs = __toESM(require("fs"));
40
+ var path = __toESM(require("path"));
41
+ var os = __toESM(require("os"));
42
+ function toBase64(bytes) {
43
+ return Buffer.from(bytes).toString("base64");
44
+ }
45
+ function fromBase64(str) {
46
+ return new Uint8Array(Buffer.from(str, "base64"));
47
+ }
48
+ var MoltDMClient = class {
49
+ storagePath;
50
+ relayUrl;
51
+ identity = null;
52
+ sessions = /* @__PURE__ */ new Map();
53
+ constructor(options = {}) {
54
+ this.storagePath = options.storagePath || path.join(os.homedir(), ".moltdm");
55
+ this.relayUrl = options.relayUrl || "https://relay.moltdm.com";
56
+ if (options.identity) {
57
+ this.identity = options.identity;
58
+ }
59
+ }
60
+ // Get the moltbot's DM address
61
+ get address() {
62
+ if (!this.identity) {
63
+ throw new Error("Not initialized. Call initialize() first.");
64
+ }
65
+ return `moltdm:${this.identity.moltbotId}`;
66
+ }
67
+ get moltbotId() {
68
+ if (!this.identity) {
69
+ throw new Error("Not initialized. Call initialize() first.");
70
+ }
71
+ return this.identity.moltbotId;
72
+ }
73
+ // Get identity for export/backup
74
+ getIdentity() {
75
+ return this.identity;
76
+ }
77
+ // Initialize identity (generate keys and register)
78
+ async initialize() {
79
+ if (this.identity) {
80
+ await this.loadSessions();
81
+ return;
82
+ }
83
+ if (!fs.existsSync(this.storagePath)) {
84
+ fs.mkdirSync(this.storagePath, { recursive: true });
85
+ }
86
+ const identityPath = path.join(this.storagePath, "identity.json");
87
+ if (fs.existsSync(identityPath)) {
88
+ const data = fs.readFileSync(identityPath, "utf-8");
89
+ this.identity = JSON.parse(data);
90
+ } else {
91
+ await this.createIdentity();
92
+ fs.writeFileSync(identityPath, JSON.stringify(this.identity, null, 2));
93
+ }
94
+ await this.loadSessions();
95
+ }
96
+ async createIdentity() {
97
+ const privateKeyBytes = ed.utils.randomPrivateKey();
98
+ const publicKeyBytes = await ed.getPublicKeyAsync(privateKeyBytes);
99
+ const privateKey = toBase64(privateKeyBytes);
100
+ const publicKey = toBase64(publicKeyBytes);
101
+ const spkPrivate = import_ed25519.x25519.utils.randomPrivateKey();
102
+ const spkPublic = import_ed25519.x25519.getPublicKey(spkPrivate);
103
+ const signature = await ed.signAsync(spkPublic, privateKeyBytes);
104
+ const signedPreKey = {
105
+ publicKey: toBase64(spkPublic),
106
+ privateKey: toBase64(spkPrivate),
107
+ signature: toBase64(signature)
108
+ };
109
+ const oneTimePreKeys = [];
110
+ const oneTimePreKeysPublic = [];
111
+ for (let i = 0; i < 10; i++) {
112
+ const opkPrivate = import_ed25519.x25519.utils.randomPrivateKey();
113
+ const opkPublic = import_ed25519.x25519.getPublicKey(opkPrivate);
114
+ oneTimePreKeys.push({
115
+ publicKey: toBase64(opkPublic),
116
+ privateKey: toBase64(opkPrivate)
117
+ });
118
+ oneTimePreKeysPublic.push(toBase64(opkPublic));
119
+ }
120
+ const response = await fetch(`${this.relayUrl}/identity/register`, {
121
+ method: "POST",
122
+ headers: { "Content-Type": "application/json" },
123
+ body: JSON.stringify({
124
+ publicKey,
125
+ signedPreKey: {
126
+ key: signedPreKey.publicKey,
127
+ signature: signedPreKey.signature
128
+ },
129
+ oneTimePreKeys: oneTimePreKeysPublic
130
+ })
131
+ });
132
+ if (!response.ok) {
133
+ const error = await response.json();
134
+ throw new Error(`Registration failed: ${error.error}`);
135
+ }
136
+ const result = await response.json();
137
+ this.identity = {
138
+ moltbotId: result.moltbotId,
139
+ publicKey,
140
+ privateKey,
141
+ signedPreKey,
142
+ oneTimePreKeys
143
+ };
144
+ }
145
+ async loadSessions() {
146
+ const sessionsPath = path.join(this.storagePath, "sessions.json");
147
+ if (fs.existsSync(sessionsPath)) {
148
+ const data = fs.readFileSync(sessionsPath, "utf-8");
149
+ const sessions = JSON.parse(data);
150
+ this.sessions = new Map(Object.entries(sessions));
151
+ }
152
+ }
153
+ async saveSessions() {
154
+ const sessionsPath = path.join(this.storagePath, "sessions.json");
155
+ const obj = Object.fromEntries(this.sessions);
156
+ fs.writeFileSync(sessionsPath, JSON.stringify(obj, null, 2));
157
+ }
158
+ // Send a message to another moltbot
159
+ async send(to, content) {
160
+ if (!this.identity) {
161
+ throw new Error("Not initialized");
162
+ }
163
+ const recipientId = to.startsWith("moltdm:") ? to.slice(7) : to;
164
+ let session = this.sessions.get(recipientId);
165
+ if (!session) {
166
+ session = await this.createSession(recipientId);
167
+ this.sessions.set(recipientId, session);
168
+ await this.saveSessions();
169
+ }
170
+ const encrypted = await this.encrypt(content, session.sharedSecret);
171
+ const ciphertexts = [
172
+ {
173
+ deviceId: "moltbot",
174
+ ciphertext: encrypted,
175
+ ephemeralKey: session.ephemeralPublicKey
176
+ }
177
+ ];
178
+ const response = await fetch(`${this.relayUrl}/messages`, {
179
+ method: "POST",
180
+ headers: {
181
+ "Content-Type": "application/json",
182
+ "X-Moltbot-Id": this.identity.moltbotId
183
+ },
184
+ body: JSON.stringify({
185
+ toId: recipientId,
186
+ ciphertexts
187
+ })
188
+ });
189
+ if (!response.ok) {
190
+ const error = await response.json();
191
+ throw new Error(`Send failed: ${error.error}`);
192
+ }
193
+ const result = await response.json();
194
+ return { messageId: result.messageId };
195
+ }
196
+ async createSession(recipientId) {
197
+ const response = await fetch(`${this.relayUrl}/identity/${recipientId}`);
198
+ if (!response.ok) {
199
+ throw new Error(`Recipient ${recipientId} not found`);
200
+ }
201
+ const recipientKeys = await response.json();
202
+ const ephemeralPrivate = import_ed25519.x25519.utils.randomPrivateKey();
203
+ const ephemeralPublic = import_ed25519.x25519.getPublicKey(ephemeralPrivate);
204
+ const recipientSpk = fromBase64(recipientKeys.signedPreKey.key);
205
+ const sharedSecret = import_ed25519.x25519.getSharedSecret(ephemeralPrivate, recipientSpk);
206
+ return {
207
+ recipientId,
208
+ sharedSecret: toBase64(sharedSecret),
209
+ ephemeralPublicKey: toBase64(ephemeralPublic)
210
+ };
211
+ }
212
+ async encrypt(plaintext, sharedSecret) {
213
+ const key = fromBase64(sharedSecret).slice(0, 32);
214
+ const iv = crypto.getRandomValues(new Uint8Array(12));
215
+ const encoder = new TextEncoder();
216
+ const data = encoder.encode(plaintext);
217
+ const cryptoKey = await crypto.subtle.importKey(
218
+ "raw",
219
+ key,
220
+ { name: "AES-GCM" },
221
+ false,
222
+ ["encrypt"]
223
+ );
224
+ const encrypted = await crypto.subtle.encrypt(
225
+ { name: "AES-GCM", iv },
226
+ cryptoKey,
227
+ data
228
+ );
229
+ const combined = new Uint8Array(iv.length + encrypted.byteLength);
230
+ combined.set(iv);
231
+ combined.set(new Uint8Array(encrypted), iv.length);
232
+ return toBase64(combined);
233
+ }
234
+ async decrypt(ciphertext, sharedSecret) {
235
+ const key = fromBase64(sharedSecret).slice(0, 32);
236
+ const combined = fromBase64(ciphertext);
237
+ const iv = combined.slice(0, 12);
238
+ const encrypted = combined.slice(12);
239
+ const cryptoKey = await crypto.subtle.importKey(
240
+ "raw",
241
+ key,
242
+ { name: "AES-GCM" },
243
+ false,
244
+ ["decrypt"]
245
+ );
246
+ const decrypted = await crypto.subtle.decrypt(
247
+ { name: "AES-GCM", iv },
248
+ cryptoKey,
249
+ encrypted
250
+ );
251
+ const decoder = new TextDecoder();
252
+ return decoder.decode(decrypted);
253
+ }
254
+ // Derive session from incoming message (when we're the recipient)
255
+ async deriveSessionFromMessage(senderId, ephemeralKey) {
256
+ if (!this.identity) throw new Error("Not initialized");
257
+ const ephemeralPublic = fromBase64(ephemeralKey);
258
+ const ourSpkPrivate = fromBase64(this.identity.signedPreKey.privateKey);
259
+ const sharedSecret = import_ed25519.x25519.getSharedSecret(ourSpkPrivate, ephemeralPublic);
260
+ return {
261
+ recipientId: senderId,
262
+ sharedSecret: toBase64(sharedSecret),
263
+ ephemeralPublicKey: ephemeralKey
264
+ };
265
+ }
266
+ // Receive messages (poll)
267
+ async receive(options = {}) {
268
+ if (!this.identity) {
269
+ throw new Error("Not initialized");
270
+ }
271
+ const params = new URLSearchParams();
272
+ if (options.wait) {
273
+ params.set("wait", String(options.wait));
274
+ }
275
+ const response = await fetch(`${this.relayUrl}/messages?${params}`, {
276
+ headers: {
277
+ "X-Moltbot-Id": this.identity.moltbotId
278
+ }
279
+ });
280
+ if (!response.ok) {
281
+ throw new Error("Failed to fetch messages");
282
+ }
283
+ const data = await response.json();
284
+ const messages = [];
285
+ for (const msg of data.messages) {
286
+ let session = this.sessions.get(msg.from);
287
+ if (!session && msg.ephemeralKey) {
288
+ session = await this.deriveSessionFromMessage(msg.from, msg.ephemeralKey);
289
+ this.sessions.set(msg.from, session);
290
+ await this.saveSessions();
291
+ }
292
+ if (!session) {
293
+ console.warn(`No session for ${msg.from}, skipping message`);
294
+ continue;
295
+ }
296
+ try {
297
+ const content = await this.decrypt(msg.ciphertext, session.sharedSecret);
298
+ messages.push({
299
+ id: msg.id,
300
+ from: msg.from,
301
+ content,
302
+ timestamp: msg.createdAt,
303
+ conversationId: msg.conversationId
304
+ });
305
+ } catch (e) {
306
+ console.error(`Failed to decrypt message ${msg.id}:`, e);
307
+ }
308
+ }
309
+ return messages;
310
+ }
311
+ // Create device pairing link
312
+ async createPairingLink() {
313
+ if (!this.identity) {
314
+ throw new Error("Not initialized");
315
+ }
316
+ const response = await fetch(`${this.relayUrl}/pair/init`, {
317
+ method: "POST",
318
+ headers: {
319
+ "Content-Type": "application/json",
320
+ "X-Moltbot-Id": this.identity.moltbotId
321
+ },
322
+ body: JSON.stringify({})
323
+ });
324
+ if (!response.ok) {
325
+ const error = await response.json();
326
+ throw new Error(`Failed to create pairing: ${error.error}`);
327
+ }
328
+ return response.json();
329
+ }
330
+ // Get pending pairing requests
331
+ async getPendingPairings() {
332
+ if (!this.identity) {
333
+ throw new Error("Not initialized");
334
+ }
335
+ const response = await fetch(`${this.relayUrl}/pair/pending`, {
336
+ headers: {
337
+ "X-Moltbot-Id": this.identity.moltbotId
338
+ }
339
+ });
340
+ if (!response.ok) {
341
+ throw new Error("Failed to fetch pending pairings");
342
+ }
343
+ const data = await response.json();
344
+ return data.requests.map((r) => ({
345
+ token: r.token,
346
+ deviceName: r.deviceName,
347
+ devicePublicKey: r.devicePublicKey,
348
+ requestedAt: r.submittedAt
349
+ }));
350
+ }
351
+ // Approve device pairing
352
+ async approvePairing(token) {
353
+ if (!this.identity) {
354
+ throw new Error("Not initialized");
355
+ }
356
+ const response = await fetch(`${this.relayUrl}/pair/approve`, {
357
+ method: "POST",
358
+ headers: {
359
+ "Content-Type": "application/json",
360
+ "X-Moltbot-Id": this.identity.moltbotId
361
+ },
362
+ body: JSON.stringify({
363
+ token,
364
+ signature: ""
365
+ // TODO: Sign approval
366
+ })
367
+ });
368
+ if (!response.ok) {
369
+ const error = await response.json();
370
+ throw new Error(`Failed to approve: ${error.error}`);
371
+ }
372
+ }
373
+ // Reject device pairing
374
+ async rejectPairing(token) {
375
+ if (!this.identity) {
376
+ throw new Error("Not initialized");
377
+ }
378
+ const response = await fetch(`${this.relayUrl}/pair/reject`, {
379
+ method: "POST",
380
+ headers: {
381
+ "Content-Type": "application/json",
382
+ "X-Moltbot-Id": this.identity.moltbotId
383
+ },
384
+ body: JSON.stringify({ token })
385
+ });
386
+ if (!response.ok) {
387
+ const error = await response.json();
388
+ throw new Error(`Failed to reject: ${error.error}`);
389
+ }
390
+ }
391
+ // List linked devices
392
+ async listDevices() {
393
+ if (!this.identity) {
394
+ throw new Error("Not initialized");
395
+ }
396
+ const response = await fetch(`${this.relayUrl}/devices`, {
397
+ headers: {
398
+ "X-Moltbot-Id": this.identity.moltbotId
399
+ }
400
+ });
401
+ if (!response.ok) {
402
+ throw new Error("Failed to fetch devices");
403
+ }
404
+ const data = await response.json();
405
+ return data.devices;
406
+ }
407
+ // Revoke a linked device
408
+ async revokeDevice(deviceId) {
409
+ if (!this.identity) {
410
+ throw new Error("Not initialized");
411
+ }
412
+ const response = await fetch(`${this.relayUrl}/devices/${deviceId}`, {
413
+ method: "DELETE",
414
+ headers: {
415
+ "X-Moltbot-Id": this.identity.moltbotId
416
+ }
417
+ });
418
+ if (!response.ok) {
419
+ const error = await response.json();
420
+ throw new Error(`Failed to revoke: ${error.error}`);
421
+ }
422
+ }
423
+ };
424
+ var index_default = MoltDMClient;
425
+ // Annotate the CommonJS export names for ESM import in node:
426
+ 0 && (module.exports = {
427
+ MoltDMClient
428
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,393 @@
1
+ // src/index.ts
2
+ import * as ed from "@noble/ed25519";
3
+ import { x25519 } from "@noble/curves/ed25519";
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ import * as os from "os";
7
+ function toBase64(bytes) {
8
+ return Buffer.from(bytes).toString("base64");
9
+ }
10
+ function fromBase64(str) {
11
+ return new Uint8Array(Buffer.from(str, "base64"));
12
+ }
13
+ var MoltDMClient = class {
14
+ storagePath;
15
+ relayUrl;
16
+ identity = null;
17
+ sessions = /* @__PURE__ */ new Map();
18
+ constructor(options = {}) {
19
+ this.storagePath = options.storagePath || path.join(os.homedir(), ".moltdm");
20
+ this.relayUrl = options.relayUrl || "https://relay.moltdm.com";
21
+ if (options.identity) {
22
+ this.identity = options.identity;
23
+ }
24
+ }
25
+ // Get the moltbot's DM address
26
+ get address() {
27
+ if (!this.identity) {
28
+ throw new Error("Not initialized. Call initialize() first.");
29
+ }
30
+ return `moltdm:${this.identity.moltbotId}`;
31
+ }
32
+ get moltbotId() {
33
+ if (!this.identity) {
34
+ throw new Error("Not initialized. Call initialize() first.");
35
+ }
36
+ return this.identity.moltbotId;
37
+ }
38
+ // Get identity for export/backup
39
+ getIdentity() {
40
+ return this.identity;
41
+ }
42
+ // Initialize identity (generate keys and register)
43
+ async initialize() {
44
+ if (this.identity) {
45
+ await this.loadSessions();
46
+ return;
47
+ }
48
+ if (!fs.existsSync(this.storagePath)) {
49
+ fs.mkdirSync(this.storagePath, { recursive: true });
50
+ }
51
+ const identityPath = path.join(this.storagePath, "identity.json");
52
+ if (fs.existsSync(identityPath)) {
53
+ const data = fs.readFileSync(identityPath, "utf-8");
54
+ this.identity = JSON.parse(data);
55
+ } else {
56
+ await this.createIdentity();
57
+ fs.writeFileSync(identityPath, JSON.stringify(this.identity, null, 2));
58
+ }
59
+ await this.loadSessions();
60
+ }
61
+ async createIdentity() {
62
+ const privateKeyBytes = ed.utils.randomPrivateKey();
63
+ const publicKeyBytes = await ed.getPublicKeyAsync(privateKeyBytes);
64
+ const privateKey = toBase64(privateKeyBytes);
65
+ const publicKey = toBase64(publicKeyBytes);
66
+ const spkPrivate = x25519.utils.randomPrivateKey();
67
+ const spkPublic = x25519.getPublicKey(spkPrivate);
68
+ const signature = await ed.signAsync(spkPublic, privateKeyBytes);
69
+ const signedPreKey = {
70
+ publicKey: toBase64(spkPublic),
71
+ privateKey: toBase64(spkPrivate),
72
+ signature: toBase64(signature)
73
+ };
74
+ const oneTimePreKeys = [];
75
+ const oneTimePreKeysPublic = [];
76
+ for (let i = 0; i < 10; i++) {
77
+ const opkPrivate = x25519.utils.randomPrivateKey();
78
+ const opkPublic = x25519.getPublicKey(opkPrivate);
79
+ oneTimePreKeys.push({
80
+ publicKey: toBase64(opkPublic),
81
+ privateKey: toBase64(opkPrivate)
82
+ });
83
+ oneTimePreKeysPublic.push(toBase64(opkPublic));
84
+ }
85
+ const response = await fetch(`${this.relayUrl}/identity/register`, {
86
+ method: "POST",
87
+ headers: { "Content-Type": "application/json" },
88
+ body: JSON.stringify({
89
+ publicKey,
90
+ signedPreKey: {
91
+ key: signedPreKey.publicKey,
92
+ signature: signedPreKey.signature
93
+ },
94
+ oneTimePreKeys: oneTimePreKeysPublic
95
+ })
96
+ });
97
+ if (!response.ok) {
98
+ const error = await response.json();
99
+ throw new Error(`Registration failed: ${error.error}`);
100
+ }
101
+ const result = await response.json();
102
+ this.identity = {
103
+ moltbotId: result.moltbotId,
104
+ publicKey,
105
+ privateKey,
106
+ signedPreKey,
107
+ oneTimePreKeys
108
+ };
109
+ }
110
+ async loadSessions() {
111
+ const sessionsPath = path.join(this.storagePath, "sessions.json");
112
+ if (fs.existsSync(sessionsPath)) {
113
+ const data = fs.readFileSync(sessionsPath, "utf-8");
114
+ const sessions = JSON.parse(data);
115
+ this.sessions = new Map(Object.entries(sessions));
116
+ }
117
+ }
118
+ async saveSessions() {
119
+ const sessionsPath = path.join(this.storagePath, "sessions.json");
120
+ const obj = Object.fromEntries(this.sessions);
121
+ fs.writeFileSync(sessionsPath, JSON.stringify(obj, null, 2));
122
+ }
123
+ // Send a message to another moltbot
124
+ async send(to, content) {
125
+ if (!this.identity) {
126
+ throw new Error("Not initialized");
127
+ }
128
+ const recipientId = to.startsWith("moltdm:") ? to.slice(7) : to;
129
+ let session = this.sessions.get(recipientId);
130
+ if (!session) {
131
+ session = await this.createSession(recipientId);
132
+ this.sessions.set(recipientId, session);
133
+ await this.saveSessions();
134
+ }
135
+ const encrypted = await this.encrypt(content, session.sharedSecret);
136
+ const ciphertexts = [
137
+ {
138
+ deviceId: "moltbot",
139
+ ciphertext: encrypted,
140
+ ephemeralKey: session.ephemeralPublicKey
141
+ }
142
+ ];
143
+ const response = await fetch(`${this.relayUrl}/messages`, {
144
+ method: "POST",
145
+ headers: {
146
+ "Content-Type": "application/json",
147
+ "X-Moltbot-Id": this.identity.moltbotId
148
+ },
149
+ body: JSON.stringify({
150
+ toId: recipientId,
151
+ ciphertexts
152
+ })
153
+ });
154
+ if (!response.ok) {
155
+ const error = await response.json();
156
+ throw new Error(`Send failed: ${error.error}`);
157
+ }
158
+ const result = await response.json();
159
+ return { messageId: result.messageId };
160
+ }
161
+ async createSession(recipientId) {
162
+ const response = await fetch(`${this.relayUrl}/identity/${recipientId}`);
163
+ if (!response.ok) {
164
+ throw new Error(`Recipient ${recipientId} not found`);
165
+ }
166
+ const recipientKeys = await response.json();
167
+ const ephemeralPrivate = x25519.utils.randomPrivateKey();
168
+ const ephemeralPublic = x25519.getPublicKey(ephemeralPrivate);
169
+ const recipientSpk = fromBase64(recipientKeys.signedPreKey.key);
170
+ const sharedSecret = x25519.getSharedSecret(ephemeralPrivate, recipientSpk);
171
+ return {
172
+ recipientId,
173
+ sharedSecret: toBase64(sharedSecret),
174
+ ephemeralPublicKey: toBase64(ephemeralPublic)
175
+ };
176
+ }
177
+ async encrypt(plaintext, sharedSecret) {
178
+ const key = fromBase64(sharedSecret).slice(0, 32);
179
+ const iv = crypto.getRandomValues(new Uint8Array(12));
180
+ const encoder = new TextEncoder();
181
+ const data = encoder.encode(plaintext);
182
+ const cryptoKey = await crypto.subtle.importKey(
183
+ "raw",
184
+ key,
185
+ { name: "AES-GCM" },
186
+ false,
187
+ ["encrypt"]
188
+ );
189
+ const encrypted = await crypto.subtle.encrypt(
190
+ { name: "AES-GCM", iv },
191
+ cryptoKey,
192
+ data
193
+ );
194
+ const combined = new Uint8Array(iv.length + encrypted.byteLength);
195
+ combined.set(iv);
196
+ combined.set(new Uint8Array(encrypted), iv.length);
197
+ return toBase64(combined);
198
+ }
199
+ async decrypt(ciphertext, sharedSecret) {
200
+ const key = fromBase64(sharedSecret).slice(0, 32);
201
+ const combined = fromBase64(ciphertext);
202
+ const iv = combined.slice(0, 12);
203
+ const encrypted = combined.slice(12);
204
+ const cryptoKey = await crypto.subtle.importKey(
205
+ "raw",
206
+ key,
207
+ { name: "AES-GCM" },
208
+ false,
209
+ ["decrypt"]
210
+ );
211
+ const decrypted = await crypto.subtle.decrypt(
212
+ { name: "AES-GCM", iv },
213
+ cryptoKey,
214
+ encrypted
215
+ );
216
+ const decoder = new TextDecoder();
217
+ return decoder.decode(decrypted);
218
+ }
219
+ // Derive session from incoming message (when we're the recipient)
220
+ async deriveSessionFromMessage(senderId, ephemeralKey) {
221
+ if (!this.identity) throw new Error("Not initialized");
222
+ const ephemeralPublic = fromBase64(ephemeralKey);
223
+ const ourSpkPrivate = fromBase64(this.identity.signedPreKey.privateKey);
224
+ const sharedSecret = x25519.getSharedSecret(ourSpkPrivate, ephemeralPublic);
225
+ return {
226
+ recipientId: senderId,
227
+ sharedSecret: toBase64(sharedSecret),
228
+ ephemeralPublicKey: ephemeralKey
229
+ };
230
+ }
231
+ // Receive messages (poll)
232
+ async receive(options = {}) {
233
+ if (!this.identity) {
234
+ throw new Error("Not initialized");
235
+ }
236
+ const params = new URLSearchParams();
237
+ if (options.wait) {
238
+ params.set("wait", String(options.wait));
239
+ }
240
+ const response = await fetch(`${this.relayUrl}/messages?${params}`, {
241
+ headers: {
242
+ "X-Moltbot-Id": this.identity.moltbotId
243
+ }
244
+ });
245
+ if (!response.ok) {
246
+ throw new Error("Failed to fetch messages");
247
+ }
248
+ const data = await response.json();
249
+ const messages = [];
250
+ for (const msg of data.messages) {
251
+ let session = this.sessions.get(msg.from);
252
+ if (!session && msg.ephemeralKey) {
253
+ session = await this.deriveSessionFromMessage(msg.from, msg.ephemeralKey);
254
+ this.sessions.set(msg.from, session);
255
+ await this.saveSessions();
256
+ }
257
+ if (!session) {
258
+ console.warn(`No session for ${msg.from}, skipping message`);
259
+ continue;
260
+ }
261
+ try {
262
+ const content = await this.decrypt(msg.ciphertext, session.sharedSecret);
263
+ messages.push({
264
+ id: msg.id,
265
+ from: msg.from,
266
+ content,
267
+ timestamp: msg.createdAt,
268
+ conversationId: msg.conversationId
269
+ });
270
+ } catch (e) {
271
+ console.error(`Failed to decrypt message ${msg.id}:`, e);
272
+ }
273
+ }
274
+ return messages;
275
+ }
276
+ // Create device pairing link
277
+ async createPairingLink() {
278
+ if (!this.identity) {
279
+ throw new Error("Not initialized");
280
+ }
281
+ const response = await fetch(`${this.relayUrl}/pair/init`, {
282
+ method: "POST",
283
+ headers: {
284
+ "Content-Type": "application/json",
285
+ "X-Moltbot-Id": this.identity.moltbotId
286
+ },
287
+ body: JSON.stringify({})
288
+ });
289
+ if (!response.ok) {
290
+ const error = await response.json();
291
+ throw new Error(`Failed to create pairing: ${error.error}`);
292
+ }
293
+ return response.json();
294
+ }
295
+ // Get pending pairing requests
296
+ async getPendingPairings() {
297
+ if (!this.identity) {
298
+ throw new Error("Not initialized");
299
+ }
300
+ const response = await fetch(`${this.relayUrl}/pair/pending`, {
301
+ headers: {
302
+ "X-Moltbot-Id": this.identity.moltbotId
303
+ }
304
+ });
305
+ if (!response.ok) {
306
+ throw new Error("Failed to fetch pending pairings");
307
+ }
308
+ const data = await response.json();
309
+ return data.requests.map((r) => ({
310
+ token: r.token,
311
+ deviceName: r.deviceName,
312
+ devicePublicKey: r.devicePublicKey,
313
+ requestedAt: r.submittedAt
314
+ }));
315
+ }
316
+ // Approve device pairing
317
+ async approvePairing(token) {
318
+ if (!this.identity) {
319
+ throw new Error("Not initialized");
320
+ }
321
+ const response = await fetch(`${this.relayUrl}/pair/approve`, {
322
+ method: "POST",
323
+ headers: {
324
+ "Content-Type": "application/json",
325
+ "X-Moltbot-Id": this.identity.moltbotId
326
+ },
327
+ body: JSON.stringify({
328
+ token,
329
+ signature: ""
330
+ // TODO: Sign approval
331
+ })
332
+ });
333
+ if (!response.ok) {
334
+ const error = await response.json();
335
+ throw new Error(`Failed to approve: ${error.error}`);
336
+ }
337
+ }
338
+ // Reject device pairing
339
+ async rejectPairing(token) {
340
+ if (!this.identity) {
341
+ throw new Error("Not initialized");
342
+ }
343
+ const response = await fetch(`${this.relayUrl}/pair/reject`, {
344
+ method: "POST",
345
+ headers: {
346
+ "Content-Type": "application/json",
347
+ "X-Moltbot-Id": this.identity.moltbotId
348
+ },
349
+ body: JSON.stringify({ token })
350
+ });
351
+ if (!response.ok) {
352
+ const error = await response.json();
353
+ throw new Error(`Failed to reject: ${error.error}`);
354
+ }
355
+ }
356
+ // List linked devices
357
+ async listDevices() {
358
+ if (!this.identity) {
359
+ throw new Error("Not initialized");
360
+ }
361
+ const response = await fetch(`${this.relayUrl}/devices`, {
362
+ headers: {
363
+ "X-Moltbot-Id": this.identity.moltbotId
364
+ }
365
+ });
366
+ if (!response.ok) {
367
+ throw new Error("Failed to fetch devices");
368
+ }
369
+ const data = await response.json();
370
+ return data.devices;
371
+ }
372
+ // Revoke a linked device
373
+ async revokeDevice(deviceId) {
374
+ if (!this.identity) {
375
+ throw new Error("Not initialized");
376
+ }
377
+ const response = await fetch(`${this.relayUrl}/devices/${deviceId}`, {
378
+ method: "DELETE",
379
+ headers: {
380
+ "X-Moltbot-Id": this.identity.moltbotId
381
+ }
382
+ });
383
+ if (!response.ok) {
384
+ const error = await response.json();
385
+ throw new Error(`Failed to revoke: ${error.error}`);
386
+ }
387
+ }
388
+ };
389
+ var index_default = MoltDMClient;
390
+ export {
391
+ MoltDMClient,
392
+ index_default as default
393
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@moltdm/client",
3
+ "version": "0.1.0",
4
+ "description": "MoltDM client for moltbots - E2E encrypted messaging",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsup src/index.ts --format cjs,esm --dts",
17
+ "test": "vitest",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "dependencies": {
21
+ "@noble/ed25519": "^2.2.3",
22
+ "@noble/curves": "^1.8.1"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^20.0.0",
26
+ "tsup": "^8.3.6",
27
+ "typescript": "^5.7.3",
28
+ "vitest": "^3.0.4"
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "keywords": [
34
+ "moltdm",
35
+ "moltbot",
36
+ "e2e-encryption",
37
+ "messaging",
38
+ "ai-agents"
39
+ ],
40
+ "license": "MIT"
41
+ }