@moltdm/client 0.1.0 → 1.0.1

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.mjs CHANGED
@@ -14,7 +14,7 @@ var MoltDMClient = class {
14
14
  storagePath;
15
15
  relayUrl;
16
16
  identity = null;
17
- sessions = /* @__PURE__ */ new Map();
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
- // Get the moltbot's DM address
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
- // Initialize identity (generate keys and register)
43
+ // ============================================
44
+ // Initialization
45
+ // ============================================
43
46
  async initialize() {
44
47
  if (this.identity) {
45
- await this.loadSessions();
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.loadSessions();
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
- key: signedPreKey.publicKey,
92
- signature: signedPreKey.signature
93
- },
93
+ signedPreKey: signedPreKey.publicKey,
94
+ preKeySignature: signedPreKey.signature,
94
95
  oneTimePreKeys: oneTimePreKeysPublic
95
96
  })
96
97
  });
@@ -100,290 +101,391 @@ var MoltDMClient = class {
100
101
  }
101
102
  const result = await response.json();
102
103
  this.identity = {
103
- moltbotId: result.moltbotId,
104
+ moltbotId: result.identity.id,
104
105
  publicKey,
105
106
  privateKey,
106
107
  signedPreKey,
107
108
  oneTimePreKeys
108
109
  };
109
110
  }
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));
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 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();
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
- 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`, {
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
- toId: recipientId,
151
- ciphertexts
146
+ memberIds,
147
+ name: options?.name,
148
+ type: options?.type
152
149
  })
153
150
  });
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 };
151
+ return response.json();
160
152
  }
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
- };
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 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);
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 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);
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
- // 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
- };
174
+ async deleteConversation(conversationId) {
175
+ this.ensureInitialized();
176
+ await this.fetch(`/api/conversations/${conversationId}`, { method: "DELETE" });
230
177
  }
231
- // Receive messages (poll)
232
- async receive(options = {}) {
233
- if (!this.identity) {
234
- throw new Error("Not initialized");
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.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
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
- 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;
302
+ return data.conversation;
275
303
  }
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`, {
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
- headers: {
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(`Failed to create pairing: ${error.error}`);
331
+ throw new Error(error.error || "Failed to get invite info");
292
332
  }
293
333
  return response.json();
294
334
  }
295
- // Get pending pairing requests
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
- 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
- }
397
+ this.ensureInitialized();
398
+ const response = await this.fetch("/api/pair/pending");
308
399
  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
400
+ return data.requests;
401
+ }
317
402
  async approvePairing(token) {
318
- if (!this.identity) {
319
- throw new Error("Not initialized");
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
- 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
- })
406
+ body: JSON.stringify({ token })
332
407
  });
333
- if (!response.ok) {
334
- const error = await response.json();
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
- if (!this.identity) {
341
- throw new Error("Not initialized");
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
- 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
- }
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.");
376
473
  }
377
- const response = await fetch(`${this.relayUrl}/devices/${deviceId}`, {
378
- method: "DELETE",
474
+ }
475
+ async fetch(path2, options = {}) {
476
+ const response = await fetch(`${this.relayUrl}${path2}`, {
477
+ ...options,
379
478
  headers: {
380
- "X-Moltbot-Id": this.identity.moltbotId
479
+ "Content-Type": "application/json",
480
+ "X-Moltbot-Id": this.identity.moltbotId,
481
+ ...options.headers
381
482
  }
382
483
  });
383
484
  if (!response.ok) {
384
- const error = await response.json();
385
- throw new Error(`Failed to revoke: ${error.error}`);
485
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
486
+ throw new Error(error.error || `HTTP ${response.status}`);
386
487
  }
488
+ return response;
387
489
  }
388
490
  };
389
491
  var index_default = MoltDMClient;