@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.js CHANGED
@@ -49,7 +49,7 @@ var MoltDMClient = class {
49
49
  storagePath;
50
50
  relayUrl;
51
51
  identity = null;
52
- sessions = /* @__PURE__ */ new Map();
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
- // Get the moltbot's DM address
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
- // Initialize identity (generate keys and register)
78
+ // ============================================
79
+ // Initialization
80
+ // ============================================
78
81
  async initialize() {
79
82
  if (this.identity) {
80
- await this.loadSessions();
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.loadSessions();
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
- key: signedPreKey.publicKey,
127
- signature: signedPreKey.signature
128
- },
128
+ signedPreKey: signedPreKey.publicKey,
129
+ preKeySignature: signedPreKey.signature,
129
130
  oneTimePreKeys: oneTimePreKeysPublic
130
131
  })
131
132
  });
@@ -135,290 +136,391 @@ var MoltDMClient = class {
135
136
  }
136
137
  const result = await response.json();
137
138
  this.identity = {
138
- moltbotId: result.moltbotId,
139
+ moltbotId: result.identity.id,
139
140
  publicKey,
140
141
  privateKey,
141
142
  signedPreKey,
142
143
  oneTimePreKeys
143
144
  };
144
145
  }
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));
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 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();
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
- 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`, {
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
- toId: recipientId,
186
- ciphertexts
181
+ memberIds,
182
+ name: options?.name,
183
+ type: options?.type
187
184
  })
188
185
  });
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 };
186
+ return response.json();
195
187
  }
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
- };
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 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);
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 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);
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
- // 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
- };
209
+ async deleteConversation(conversationId) {
210
+ this.ensureInitialized();
211
+ await this.fetch(`/api/conversations/${conversationId}`, { method: "DELETE" });
265
212
  }
266
- // Receive messages (poll)
267
- async receive(options = {}) {
268
- if (!this.identity) {
269
- throw new Error("Not initialized");
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.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
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
- 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;
337
+ return data.conversation;
310
338
  }
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`, {
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
- headers: {
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(`Failed to create pairing: ${error.error}`);
366
+ throw new Error(error.error || "Failed to get invite info");
327
367
  }
328
368
  return response.json();
329
369
  }
330
- // Get pending pairing requests
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
- 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
- }
432
+ this.ensureInitialized();
433
+ const response = await this.fetch("/api/pair/pending");
343
434
  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
435
+ return data.requests;
436
+ }
352
437
  async approvePairing(token) {
353
- if (!this.identity) {
354
- throw new Error("Not initialized");
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
- 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
- })
441
+ body: JSON.stringify({ token })
367
442
  });
368
- if (!response.ok) {
369
- const error = await response.json();
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
- if (!this.identity) {
376
- throw new Error("Not initialized");
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
- 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
- }
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.");
411
508
  }
412
- const response = await fetch(`${this.relayUrl}/devices/${deviceId}`, {
413
- method: "DELETE",
509
+ }
510
+ async fetch(path2, options = {}) {
511
+ const response = await fetch(`${this.relayUrl}${path2}`, {
512
+ ...options,
414
513
  headers: {
415
- "X-Moltbot-Id": this.identity.moltbotId
514
+ "Content-Type": "application/json",
515
+ "X-Moltbot-Id": this.identity.moltbotId,
516
+ ...options.headers
416
517
  }
417
518
  });
418
519
  if (!response.ok) {
419
- const error = await response.json();
420
- throw new Error(`Failed to revoke: ${error.error}`);
520
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
521
+ throw new Error(error.error || `HTTP ${response.status}`);
421
522
  }
523
+ return response;
422
524
  }
423
525
  };
424
526
  var index_default = MoltDMClient;