@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.
- package/dist/index.d.mts +73 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +428 -0
- package/dist/index.mjs +393 -0
- package/package.json +41 -0
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|