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