@moltdm/client 0.1.0 → 1.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 +170 -28
- package/dist/index.d.ts +170 -28
- package/dist/index.js +378 -241
- package/dist/index.mjs +378 -241
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -49,7 +49,7 @@ var MoltDMClient = class {
|
|
|
49
49
|
storagePath;
|
|
50
50
|
relayUrl;
|
|
51
51
|
identity = null;
|
|
52
|
-
|
|
52
|
+
senderKeys = /* @__PURE__ */ new Map();
|
|
53
53
|
constructor(options = {}) {
|
|
54
54
|
this.storagePath = options.storagePath || path.join(os.homedir(), ".moltdm");
|
|
55
55
|
this.relayUrl = options.relayUrl || "https://relay.moltdm.com";
|
|
@@ -57,7 +57,9 @@ var MoltDMClient = class {
|
|
|
57
57
|
this.identity = options.identity;
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
//
|
|
60
|
+
// ============================================
|
|
61
|
+
// Properties
|
|
62
|
+
// ============================================
|
|
61
63
|
get address() {
|
|
62
64
|
if (!this.identity) {
|
|
63
65
|
throw new Error("Not initialized. Call initialize() first.");
|
|
@@ -70,14 +72,15 @@ var MoltDMClient = class {
|
|
|
70
72
|
}
|
|
71
73
|
return this.identity.moltbotId;
|
|
72
74
|
}
|
|
73
|
-
// Get identity for export/backup
|
|
74
75
|
getIdentity() {
|
|
75
76
|
return this.identity;
|
|
76
77
|
}
|
|
77
|
-
//
|
|
78
|
+
// ============================================
|
|
79
|
+
// Initialization
|
|
80
|
+
// ============================================
|
|
78
81
|
async initialize() {
|
|
79
82
|
if (this.identity) {
|
|
80
|
-
await this.
|
|
83
|
+
await this.loadSenderKeys();
|
|
81
84
|
return;
|
|
82
85
|
}
|
|
83
86
|
if (!fs.existsSync(this.storagePath)) {
|
|
@@ -91,7 +94,7 @@ var MoltDMClient = class {
|
|
|
91
94
|
await this.createIdentity();
|
|
92
95
|
fs.writeFileSync(identityPath, JSON.stringify(this.identity, null, 2));
|
|
93
96
|
}
|
|
94
|
-
await this.
|
|
97
|
+
await this.loadSenderKeys();
|
|
95
98
|
}
|
|
96
99
|
async createIdentity() {
|
|
97
100
|
const privateKeyBytes = ed.utils.randomPrivateKey();
|
|
@@ -117,15 +120,13 @@ var MoltDMClient = class {
|
|
|
117
120
|
});
|
|
118
121
|
oneTimePreKeysPublic.push(toBase64(opkPublic));
|
|
119
122
|
}
|
|
120
|
-
const response = await fetch(`${this.relayUrl}/identity/register`, {
|
|
123
|
+
const response = await fetch(`${this.relayUrl}/api/identity/register`, {
|
|
121
124
|
method: "POST",
|
|
122
125
|
headers: { "Content-Type": "application/json" },
|
|
123
126
|
body: JSON.stringify({
|
|
124
127
|
publicKey,
|
|
125
|
-
signedPreKey:
|
|
126
|
-
|
|
127
|
-
signature: signedPreKey.signature
|
|
128
|
-
},
|
|
128
|
+
signedPreKey: signedPreKey.publicKey,
|
|
129
|
+
preKeySignature: signedPreKey.signature,
|
|
129
130
|
oneTimePreKeys: oneTimePreKeysPublic
|
|
130
131
|
})
|
|
131
132
|
});
|
|
@@ -135,290 +136,426 @@ var MoltDMClient = class {
|
|
|
135
136
|
}
|
|
136
137
|
const result = await response.json();
|
|
137
138
|
this.identity = {
|
|
138
|
-
moltbotId: result.
|
|
139
|
+
moltbotId: result.identity.id,
|
|
139
140
|
publicKey,
|
|
140
141
|
privateKey,
|
|
141
142
|
signedPreKey,
|
|
142
143
|
oneTimePreKeys
|
|
143
144
|
};
|
|
144
145
|
}
|
|
145
|
-
async
|
|
146
|
-
const
|
|
147
|
-
if (fs.existsSync(
|
|
148
|
-
const data = fs.readFileSync(
|
|
149
|
-
const
|
|
150
|
-
|
|
146
|
+
async loadSenderKeys() {
|
|
147
|
+
const keysPath = path.join(this.storagePath, "sender_keys.json");
|
|
148
|
+
if (fs.existsSync(keysPath)) {
|
|
149
|
+
const data = fs.readFileSync(keysPath, "utf-8");
|
|
150
|
+
const keys = JSON.parse(data);
|
|
151
|
+
for (const [convId, keyData] of Object.entries(keys)) {
|
|
152
|
+
const k = keyData;
|
|
153
|
+
this.senderKeys.set(convId, {
|
|
154
|
+
key: fromBase64(k.key),
|
|
155
|
+
version: k.version,
|
|
156
|
+
index: k.index
|
|
157
|
+
});
|
|
158
|
+
}
|
|
151
159
|
}
|
|
152
160
|
}
|
|
153
|
-
async
|
|
154
|
-
const
|
|
155
|
-
const obj =
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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();
|
|
161
|
+
async saveSenderKeys() {
|
|
162
|
+
const keysPath = path.join(this.storagePath, "sender_keys.json");
|
|
163
|
+
const obj = {};
|
|
164
|
+
for (const [convId, keyData] of this.senderKeys) {
|
|
165
|
+
obj[convId] = {
|
|
166
|
+
key: toBase64(keyData.key),
|
|
167
|
+
version: keyData.version,
|
|
168
|
+
index: keyData.index
|
|
169
|
+
};
|
|
169
170
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const response = await fetch(`${this.relayUrl}/messages`, {
|
|
171
|
+
fs.writeFileSync(keysPath, JSON.stringify(obj, null, 2));
|
|
172
|
+
}
|
|
173
|
+
// ============================================
|
|
174
|
+
// Conversations
|
|
175
|
+
// ============================================
|
|
176
|
+
async startConversation(memberIds, options) {
|
|
177
|
+
this.ensureInitialized();
|
|
178
|
+
const response = await this.fetch("/api/conversations", {
|
|
179
179
|
method: "POST",
|
|
180
|
-
headers: {
|
|
181
|
-
"Content-Type": "application/json",
|
|
182
|
-
"X-Moltbot-Id": this.identity.moltbotId
|
|
183
|
-
},
|
|
184
180
|
body: JSON.stringify({
|
|
185
|
-
|
|
186
|
-
|
|
181
|
+
memberIds,
|
|
182
|
+
name: options?.name,
|
|
183
|
+
type: options?.type
|
|
187
184
|
})
|
|
188
185
|
});
|
|
189
|
-
|
|
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 };
|
|
186
|
+
return response.json();
|
|
195
187
|
}
|
|
196
|
-
async
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
};
|
|
188
|
+
async listConversations() {
|
|
189
|
+
this.ensureInitialized();
|
|
190
|
+
const response = await this.fetch("/api/conversations");
|
|
191
|
+
const data = await response.json();
|
|
192
|
+
return data.conversations;
|
|
211
193
|
}
|
|
212
|
-
async
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
|
|
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);
|
|
194
|
+
async getConversation(conversationId) {
|
|
195
|
+
this.ensureInitialized();
|
|
196
|
+
const response = await this.fetch(`/api/conversations/${conversationId}`);
|
|
197
|
+
const data = await response.json();
|
|
198
|
+
return data.conversation;
|
|
233
199
|
}
|
|
234
|
-
async
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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);
|
|
200
|
+
async updateConversation(conversationId, updates) {
|
|
201
|
+
this.ensureInitialized();
|
|
202
|
+
const response = await this.fetch(`/api/conversations/${conversationId}`, {
|
|
203
|
+
method: "PATCH",
|
|
204
|
+
body: JSON.stringify(updates)
|
|
205
|
+
});
|
|
206
|
+
const data = await response.json();
|
|
207
|
+
return data.conversation;
|
|
253
208
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
};
|
|
209
|
+
async deleteConversation(conversationId) {
|
|
210
|
+
this.ensureInitialized();
|
|
211
|
+
await this.fetch(`/api/conversations/${conversationId}`, { method: "DELETE" });
|
|
265
212
|
}
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
213
|
+
// ============================================
|
|
214
|
+
// Members
|
|
215
|
+
// ============================================
|
|
216
|
+
async addMembers(conversationId, memberIds) {
|
|
217
|
+
this.ensureInitialized();
|
|
218
|
+
const response = await this.fetch(`/api/conversations/${conversationId}/members`, {
|
|
219
|
+
method: "POST",
|
|
220
|
+
body: JSON.stringify({ memberIds })
|
|
221
|
+
});
|
|
222
|
+
const data = await response.json();
|
|
223
|
+
return data.conversation;
|
|
224
|
+
}
|
|
225
|
+
async removeMember(conversationId, memberId) {
|
|
226
|
+
this.ensureInitialized();
|
|
227
|
+
await this.fetch(`/api/conversations/${conversationId}/members/${memberId}`, {
|
|
228
|
+
method: "DELETE"
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
async leaveConversation(conversationId) {
|
|
232
|
+
this.ensureInitialized();
|
|
233
|
+
await this.removeMember(conversationId, this.moltbotId);
|
|
234
|
+
}
|
|
235
|
+
async promoteAdmin(conversationId, memberId) {
|
|
236
|
+
this.ensureInitialized();
|
|
237
|
+
const response = await this.fetch(`/api/conversations/${conversationId}/admins`, {
|
|
238
|
+
method: "POST",
|
|
239
|
+
body: JSON.stringify({ memberId })
|
|
240
|
+
});
|
|
241
|
+
const data = await response.json();
|
|
242
|
+
return data.conversation;
|
|
243
|
+
}
|
|
244
|
+
async demoteAdmin(conversationId, memberId) {
|
|
245
|
+
this.ensureInitialized();
|
|
246
|
+
const response = await this.fetch(`/api/conversations/${conversationId}/admins/${memberId}`, {
|
|
247
|
+
method: "DELETE"
|
|
248
|
+
});
|
|
249
|
+
const data = await response.json();
|
|
250
|
+
return data.conversation;
|
|
251
|
+
}
|
|
252
|
+
// ============================================
|
|
253
|
+
// Messages
|
|
254
|
+
// ============================================
|
|
255
|
+
async send(conversationId, content, options) {
|
|
256
|
+
this.ensureInitialized();
|
|
257
|
+
let senderKey = this.senderKeys.get(conversationId);
|
|
258
|
+
if (!senderKey) {
|
|
259
|
+
senderKey = {
|
|
260
|
+
key: crypto.getRandomValues(new Uint8Array(32)),
|
|
261
|
+
version: 1,
|
|
262
|
+
index: 0
|
|
263
|
+
};
|
|
264
|
+
this.senderKeys.set(conversationId, senderKey);
|
|
265
|
+
await this.saveSenderKeys();
|
|
270
266
|
}
|
|
267
|
+
const ciphertext = await this.encrypt(content, senderKey.key);
|
|
268
|
+
const response = await this.fetch(`/api/conversations/${conversationId}/messages`, {
|
|
269
|
+
method: "POST",
|
|
270
|
+
body: JSON.stringify({
|
|
271
|
+
ciphertext,
|
|
272
|
+
senderKeyVersion: senderKey.version,
|
|
273
|
+
messageIndex: senderKey.index++,
|
|
274
|
+
replyTo: options?.replyTo
|
|
275
|
+
})
|
|
276
|
+
});
|
|
277
|
+
await this.saveSenderKeys();
|
|
278
|
+
const data = await response.json();
|
|
279
|
+
return { messageId: data.message.id };
|
|
280
|
+
}
|
|
281
|
+
async getMessages(conversationId, options) {
|
|
282
|
+
this.ensureInitialized();
|
|
271
283
|
const params = new URLSearchParams();
|
|
272
|
-
if (options.
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
const response = await fetch(
|
|
276
|
-
|
|
277
|
-
|
|
284
|
+
if (options?.since) params.set("since", options.since);
|
|
285
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
286
|
+
const url = `/api/conversations/${conversationId}/messages${params.toString() ? "?" + params : ""}`;
|
|
287
|
+
const response = await this.fetch(url);
|
|
288
|
+
const data = await response.json();
|
|
289
|
+
return data.messages;
|
|
290
|
+
}
|
|
291
|
+
async deleteMessage(conversationId, messageId) {
|
|
292
|
+
this.ensureInitialized();
|
|
293
|
+
await this.fetch(`/api/conversations/${conversationId}/messages/${messageId}`, {
|
|
294
|
+
method: "DELETE"
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
// ============================================
|
|
298
|
+
// Reactions
|
|
299
|
+
// ============================================
|
|
300
|
+
async react(conversationId, messageId, emoji) {
|
|
301
|
+
this.ensureInitialized();
|
|
302
|
+
const response = await this.fetch(
|
|
303
|
+
`/api/conversations/${conversationId}/messages/${messageId}/reactions`,
|
|
304
|
+
{
|
|
305
|
+
method: "POST",
|
|
306
|
+
body: JSON.stringify({ emoji })
|
|
278
307
|
}
|
|
308
|
+
);
|
|
309
|
+
const data = await response.json();
|
|
310
|
+
return data.reaction;
|
|
311
|
+
}
|
|
312
|
+
async unreact(conversationId, messageId, emoji) {
|
|
313
|
+
this.ensureInitialized();
|
|
314
|
+
await this.fetch(
|
|
315
|
+
`/api/conversations/${conversationId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`,
|
|
316
|
+
{ method: "DELETE" }
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
async getReactions(conversationId, messageId) {
|
|
320
|
+
this.ensureInitialized();
|
|
321
|
+
const response = await this.fetch(
|
|
322
|
+
`/api/conversations/${conversationId}/messages/${messageId}/reactions`
|
|
323
|
+
);
|
|
324
|
+
const data = await response.json();
|
|
325
|
+
return data.reactions;
|
|
326
|
+
}
|
|
327
|
+
// ============================================
|
|
328
|
+
// Disappearing Messages
|
|
329
|
+
// ============================================
|
|
330
|
+
async setDisappearingTimer(conversationId, timer) {
|
|
331
|
+
this.ensureInitialized();
|
|
332
|
+
const response = await this.fetch(`/api/conversations/${conversationId}/disappearing`, {
|
|
333
|
+
method: "PATCH",
|
|
334
|
+
body: JSON.stringify({ timer })
|
|
279
335
|
});
|
|
280
|
-
if (!response.ok) {
|
|
281
|
-
throw new Error("Failed to fetch messages");
|
|
282
|
-
}
|
|
283
336
|
const data = await response.json();
|
|
284
|
-
|
|
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;
|
|
337
|
+
return data.conversation;
|
|
310
338
|
}
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const response = await fetch(
|
|
339
|
+
// ============================================
|
|
340
|
+
// Invites
|
|
341
|
+
// ============================================
|
|
342
|
+
async createInvite(conversationId, options) {
|
|
343
|
+
this.ensureInitialized();
|
|
344
|
+
const response = await this.fetch(`/api/conversations/${conversationId}/invites`, {
|
|
317
345
|
method: "POST",
|
|
318
|
-
|
|
319
|
-
"Content-Type": "application/json",
|
|
320
|
-
"X-Moltbot-Id": this.identity.moltbotId
|
|
321
|
-
},
|
|
322
|
-
body: JSON.stringify({})
|
|
346
|
+
body: JSON.stringify({ expiresIn: options?.expiresIn })
|
|
323
347
|
});
|
|
348
|
+
return response.json();
|
|
349
|
+
}
|
|
350
|
+
async listInvites(conversationId) {
|
|
351
|
+
this.ensureInitialized();
|
|
352
|
+
const response = await this.fetch(`/api/conversations/${conversationId}/invites`);
|
|
353
|
+
const data = await response.json();
|
|
354
|
+
return data.invites;
|
|
355
|
+
}
|
|
356
|
+
async revokeInvite(conversationId, token) {
|
|
357
|
+
this.ensureInitialized();
|
|
358
|
+
await this.fetch(`/api/conversations/${conversationId}/invites/${token}`, {
|
|
359
|
+
method: "DELETE"
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
async getInviteInfo(token) {
|
|
363
|
+
const response = await fetch(`${this.relayUrl}/api/invites/${token}`);
|
|
324
364
|
if (!response.ok) {
|
|
325
365
|
const error = await response.json();
|
|
326
|
-
throw new Error(
|
|
366
|
+
throw new Error(error.error || "Failed to get invite info");
|
|
327
367
|
}
|
|
328
368
|
return response.json();
|
|
329
369
|
}
|
|
330
|
-
|
|
370
|
+
async joinViaInvite(token) {
|
|
371
|
+
this.ensureInitialized();
|
|
372
|
+
const response = await this.fetch(`/api/invites/${token}/join`, { method: "POST" });
|
|
373
|
+
const data = await response.json();
|
|
374
|
+
return data.conversation;
|
|
375
|
+
}
|
|
376
|
+
// ============================================
|
|
377
|
+
// Message Requests
|
|
378
|
+
// ============================================
|
|
379
|
+
async getPendingRequests() {
|
|
380
|
+
this.ensureInitialized();
|
|
381
|
+
const response = await this.fetch("/api/requests");
|
|
382
|
+
const data = await response.json();
|
|
383
|
+
return data.requests;
|
|
384
|
+
}
|
|
385
|
+
async acceptRequest(requestId) {
|
|
386
|
+
this.ensureInitialized();
|
|
387
|
+
const response = await this.fetch(`/api/requests/${requestId}/accept`, { method: "POST" });
|
|
388
|
+
const data = await response.json();
|
|
389
|
+
return data.conversation;
|
|
390
|
+
}
|
|
391
|
+
async rejectRequest(requestId) {
|
|
392
|
+
this.ensureInitialized();
|
|
393
|
+
await this.fetch(`/api/requests/${requestId}/reject`, { method: "POST" });
|
|
394
|
+
}
|
|
395
|
+
// ============================================
|
|
396
|
+
// Blocking
|
|
397
|
+
// ============================================
|
|
398
|
+
async block(moltbotId) {
|
|
399
|
+
this.ensureInitialized();
|
|
400
|
+
await this.fetch(`/api/blocks/${moltbotId}`, { method: "POST" });
|
|
401
|
+
}
|
|
402
|
+
async unblock(moltbotId) {
|
|
403
|
+
this.ensureInitialized();
|
|
404
|
+
await this.fetch(`/api/blocks/${moltbotId}`, { method: "DELETE" });
|
|
405
|
+
}
|
|
406
|
+
async listBlocked() {
|
|
407
|
+
this.ensureInitialized();
|
|
408
|
+
const response = await this.fetch("/api/blocks");
|
|
409
|
+
const data = await response.json();
|
|
410
|
+
return data.blocked;
|
|
411
|
+
}
|
|
412
|
+
// ============================================
|
|
413
|
+
// Polling
|
|
414
|
+
// ============================================
|
|
415
|
+
async poll(options) {
|
|
416
|
+
this.ensureInitialized();
|
|
417
|
+
const params = new URLSearchParams();
|
|
418
|
+
if (options?.since) params.set("since", options.since);
|
|
419
|
+
const url = `/api/poll${params.toString() ? "?" + params : ""}`;
|
|
420
|
+
const response = await this.fetch(url);
|
|
421
|
+
return response.json();
|
|
422
|
+
}
|
|
423
|
+
// ============================================
|
|
424
|
+
// Device Pairing
|
|
425
|
+
// ============================================
|
|
426
|
+
async createPairingLink() {
|
|
427
|
+
this.ensureInitialized();
|
|
428
|
+
const response = await this.fetch("/api/pair/init", { method: "POST" });
|
|
429
|
+
return response.json();
|
|
430
|
+
}
|
|
331
431
|
async getPendingPairings() {
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
}
|
|
432
|
+
this.ensureInitialized();
|
|
433
|
+
const response = await this.fetch("/api/pair/pending");
|
|
343
434
|
const data = await response.json();
|
|
344
|
-
return data.requests
|
|
345
|
-
|
|
346
|
-
deviceName: r.deviceName,
|
|
347
|
-
devicePublicKey: r.devicePublicKey,
|
|
348
|
-
requestedAt: r.submittedAt
|
|
349
|
-
}));
|
|
350
|
-
}
|
|
351
|
-
// Approve device pairing
|
|
435
|
+
return data.requests;
|
|
436
|
+
}
|
|
352
437
|
async approvePairing(token) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
const response = await fetch(`${this.relayUrl}/pair/approve`, {
|
|
438
|
+
this.ensureInitialized();
|
|
439
|
+
const response = await this.fetch("/api/pair/approve", {
|
|
357
440
|
method: "POST",
|
|
358
|
-
|
|
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
|
-
})
|
|
441
|
+
body: JSON.stringify({ token })
|
|
367
442
|
});
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
throw new Error(`Failed to approve: ${error.error}`);
|
|
371
|
-
}
|
|
443
|
+
const data = await response.json();
|
|
444
|
+
return data.device;
|
|
372
445
|
}
|
|
373
|
-
// Reject device pairing
|
|
374
446
|
async rejectPairing(token) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
const response = await fetch(`${this.relayUrl}/pair/reject`, {
|
|
447
|
+
this.ensureInitialized();
|
|
448
|
+
await this.fetch("/api/pair/reject", {
|
|
379
449
|
method: "POST",
|
|
380
|
-
headers: {
|
|
381
|
-
"Content-Type": "application/json",
|
|
382
|
-
"X-Moltbot-Id": this.identity.moltbotId
|
|
383
|
-
},
|
|
384
450
|
body: JSON.stringify({ token })
|
|
385
451
|
});
|
|
386
|
-
if (!response.ok) {
|
|
387
|
-
const error = await response.json();
|
|
388
|
-
throw new Error(`Failed to reject: ${error.error}`);
|
|
389
|
-
}
|
|
390
452
|
}
|
|
391
|
-
// List linked devices
|
|
392
453
|
async listDevices() {
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
}
|
|
454
|
+
this.ensureInitialized();
|
|
455
|
+
const response = await this.fetch("/api/devices");
|
|
404
456
|
const data = await response.json();
|
|
405
457
|
return data.devices;
|
|
406
458
|
}
|
|
407
|
-
// Revoke a linked device
|
|
408
459
|
async revokeDevice(deviceId) {
|
|
460
|
+
this.ensureInitialized();
|
|
461
|
+
await this.fetch(`/api/devices/${deviceId}`, { method: "DELETE" });
|
|
462
|
+
}
|
|
463
|
+
// ============================================
|
|
464
|
+
// Events
|
|
465
|
+
// ============================================
|
|
466
|
+
async getEvents(conversationId, options) {
|
|
467
|
+
this.ensureInitialized();
|
|
468
|
+
const params = new URLSearchParams();
|
|
469
|
+
if (options?.since) params.set("since", options.since);
|
|
470
|
+
const url = `/api/conversations/${conversationId}/events${params.toString() ? "?" + params : ""}`;
|
|
471
|
+
const response = await this.fetch(url);
|
|
472
|
+
const data = await response.json();
|
|
473
|
+
return data.events;
|
|
474
|
+
}
|
|
475
|
+
// ============================================
|
|
476
|
+
// Encryption (Simplified for demo)
|
|
477
|
+
// ============================================
|
|
478
|
+
async encrypt(plaintext, key) {
|
|
479
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
480
|
+
const encoder = new TextEncoder();
|
|
481
|
+
const data = encoder.encode(plaintext);
|
|
482
|
+
const cryptoKey = await crypto.subtle.importKey("raw", key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength), { name: "AES-GCM" }, false, [
|
|
483
|
+
"encrypt"
|
|
484
|
+
]);
|
|
485
|
+
const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, cryptoKey, data);
|
|
486
|
+
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
|
487
|
+
combined.set(iv);
|
|
488
|
+
combined.set(new Uint8Array(encrypted), iv.length);
|
|
489
|
+
return toBase64(combined);
|
|
490
|
+
}
|
|
491
|
+
async decrypt(ciphertext, key) {
|
|
492
|
+
const combined = fromBase64(ciphertext);
|
|
493
|
+
const iv = combined.slice(0, 12);
|
|
494
|
+
const encrypted = combined.slice(12);
|
|
495
|
+
const cryptoKey = await crypto.subtle.importKey("raw", key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength), { name: "AES-GCM" }, false, [
|
|
496
|
+
"decrypt"
|
|
497
|
+
]);
|
|
498
|
+
const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, cryptoKey, encrypted);
|
|
499
|
+
const decoder = new TextDecoder();
|
|
500
|
+
return decoder.decode(decrypted);
|
|
501
|
+
}
|
|
502
|
+
// ============================================
|
|
503
|
+
// Helpers
|
|
504
|
+
// ============================================
|
|
505
|
+
ensureInitialized() {
|
|
409
506
|
if (!this.identity) {
|
|
410
|
-
throw new Error("Not initialized");
|
|
507
|
+
throw new Error("Not initialized. Call initialize() first.");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Sign a message using Ed25519
|
|
512
|
+
*/
|
|
513
|
+
async signMessage(message) {
|
|
514
|
+
const privateKeyBytes = fromBase64(this.identity.privateKey);
|
|
515
|
+
const signature = await ed.signAsync(
|
|
516
|
+
new TextEncoder().encode(message),
|
|
517
|
+
privateKeyBytes
|
|
518
|
+
);
|
|
519
|
+
return toBase64(signature);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Create the message to sign for a request
|
|
523
|
+
* Format: timestamp:method:path:bodyHash
|
|
524
|
+
*/
|
|
525
|
+
async createSignedMessage(timestamp, method, path2, body) {
|
|
526
|
+
let bodyHash = "";
|
|
527
|
+
if (body) {
|
|
528
|
+
const bodyBytes = new TextEncoder().encode(body);
|
|
529
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", bodyBytes);
|
|
530
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
531
|
+
bodyHash = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
411
532
|
}
|
|
412
|
-
|
|
413
|
-
|
|
533
|
+
return `${timestamp}:${method}:${path2}:${bodyHash}`;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Make an authenticated fetch request with Ed25519 signature
|
|
537
|
+
*/
|
|
538
|
+
async fetch(path2, options = {}) {
|
|
539
|
+
const method = options.method || "GET";
|
|
540
|
+
const body = options.body;
|
|
541
|
+
const timestamp = Date.now().toString();
|
|
542
|
+
const message = await this.createSignedMessage(timestamp, method, path2, body);
|
|
543
|
+
const signature = await this.signMessage(message);
|
|
544
|
+
const response = await fetch(`${this.relayUrl}${path2}`, {
|
|
545
|
+
...options,
|
|
414
546
|
headers: {
|
|
415
|
-
"
|
|
547
|
+
"Content-Type": "application/json",
|
|
548
|
+
"X-Moltbot-Id": this.identity.moltbotId,
|
|
549
|
+
"X-Timestamp": timestamp,
|
|
550
|
+
"X-Signature": signature,
|
|
551
|
+
...options.headers
|
|
416
552
|
}
|
|
417
553
|
});
|
|
418
554
|
if (!response.ok) {
|
|
419
|
-
const error = await response.json();
|
|
420
|
-
throw new Error(
|
|
555
|
+
const error = await response.json().catch(() => ({ error: "Request failed" }));
|
|
556
|
+
throw new Error(error.error || `HTTP ${response.status}`);
|
|
421
557
|
}
|
|
558
|
+
return response;
|
|
422
559
|
}
|
|
423
560
|
};
|
|
424
561
|
var index_default = MoltDMClient;
|