@thezelijah/majik-message 1.1.1 → 1.1.3

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.
@@ -0,0 +1,637 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import { ThreadStatus } from "./enums";
3
+ import { sha256 } from "../../crypto/crypto-provider";
4
+ // ==================== Custom Errors ====================
5
+ export class MajikThreadError extends Error {
6
+ code;
7
+ constructor(message, code) {
8
+ super(message);
9
+ this.code = code;
10
+ this.name = "MajikThreadError";
11
+ }
12
+ }
13
+ export class ValidationError extends MajikThreadError {
14
+ constructor(message) {
15
+ super(message, "VALIDATION_ERROR");
16
+ this.name = "ValidationError";
17
+ }
18
+ }
19
+ export class OperationNotAllowedError extends MajikThreadError {
20
+ constructor(message) {
21
+ super(message, "OPERATION_NOT_ALLOWED");
22
+ this.name = "OperationNotAllowedError";
23
+ }
24
+ }
25
+ // ==================== Main Class ====================
26
+ export class MajikMessageThread {
27
+ _id;
28
+ _userID;
29
+ _owner; // Owner's identity account ID
30
+ _metadata;
31
+ _timestamp;
32
+ _participants;
33
+ _status;
34
+ _hash;
35
+ _deletionApprovals;
36
+ _starred;
37
+ // ==================== Private Constructor ====================
38
+ constructor(id, userID, owner, metadata, timestamp, participants, status, hash, deletionApprovals = [], starred = false) {
39
+ this._id = id;
40
+ this._userID = userID;
41
+ this._owner = owner;
42
+ this._metadata = metadata;
43
+ this._timestamp = timestamp;
44
+ this._participants = participants;
45
+ this._status = status;
46
+ this._hash = hash;
47
+ this._deletionApprovals = deletionApprovals;
48
+ this._starred = starred;
49
+ // Validate on construction
50
+ this.validate();
51
+ }
52
+ // ==================== Getters ====================
53
+ get id() {
54
+ return this._id;
55
+ }
56
+ get userID() {
57
+ return this._userID;
58
+ }
59
+ get owner() {
60
+ return this._owner;
61
+ }
62
+ get metadata() {
63
+ return { ...this._metadata };
64
+ }
65
+ get timestamp() {
66
+ return new Date(this._timestamp);
67
+ }
68
+ get participants() {
69
+ return [...this._participants];
70
+ }
71
+ get status() {
72
+ return this._status;
73
+ }
74
+ get hash() {
75
+ return this._hash;
76
+ }
77
+ get deletionApprovals() {
78
+ return [...this._deletionApprovals];
79
+ }
80
+ get starred() {
81
+ return this._starred;
82
+ }
83
+ // ==================== Static Create Method ====================
84
+ static create(userID, owner, participants, metadata = {}) {
85
+ try {
86
+ // Validate inputs
87
+ if (!userID || typeof userID !== "string" || userID.trim().length === 0) {
88
+ throw new ValidationError("userID is required and must be a non-empty string");
89
+ }
90
+ if (!Array.isArray(participants) || participants.length === 0) {
91
+ throw new ValidationError("participants must be a non-empty array");
92
+ }
93
+ // Normalize participants (deduplicate + sort)
94
+ const uniqueParticipants = MajikMessageThread.normalizeParticipants([
95
+ owner.publicKey,
96
+ ...participants,
97
+ ]);
98
+ // Validate all participants
99
+ for (const participant of uniqueParticipants) {
100
+ if (!participant ||
101
+ typeof participant !== "string" ||
102
+ participant.trim().length === 0) {
103
+ throw new ValidationError("All participants must be non-empty strings");
104
+ }
105
+ }
106
+ const id = uuidv4();
107
+ const timestamp = new Date();
108
+ const status = ThreadStatus.ONGOING;
109
+ // Generate hash
110
+ const hash = MajikMessageThread.generateHash(userID, timestamp, id, uniqueParticipants);
111
+ return new MajikMessageThread(id, userID, owner.id, metadata, timestamp, uniqueParticipants, status, hash, []);
112
+ }
113
+ catch (error) {
114
+ if (error instanceof MajikThreadError) {
115
+ throw error;
116
+ }
117
+ throw new MajikThreadError(`Failed to create MajikMessageThread: ${error instanceof Error ? error.message : "Unknown error"}`, "CREATE_FAILED");
118
+ }
119
+ }
120
+ // ==================== Star Management ====================
121
+ /**
122
+ * Stars the thread for the user
123
+ */
124
+ star() {
125
+ try {
126
+ if (this._starred) {
127
+ throw new OperationNotAllowedError("Thread is already starred");
128
+ }
129
+ this._starred = true;
130
+ }
131
+ catch (error) {
132
+ if (error instanceof MajikThreadError) {
133
+ throw error;
134
+ }
135
+ throw new MajikThreadError(`Failed to star thread: ${error instanceof Error ? error.message : "Unknown error"}`, "STAR_FAILED");
136
+ }
137
+ }
138
+ /**
139
+ * Unstars the thread for the user
140
+ */
141
+ unstar() {
142
+ try {
143
+ if (!this._starred) {
144
+ throw new OperationNotAllowedError("Thread is not starred");
145
+ }
146
+ this._starred = false;
147
+ }
148
+ catch (error) {
149
+ if (error instanceof MajikThreadError) {
150
+ throw error;
151
+ }
152
+ throw new MajikThreadError(`Failed to unstar thread: ${error instanceof Error ? error.message : "Unknown error"}`, "UNSTAR_FAILED");
153
+ }
154
+ }
155
+ /**
156
+ * Toggles the starred status of the thread
157
+ * @returns The new starred state
158
+ */
159
+ toggleStar() {
160
+ if (this._starred) {
161
+ this.unstar();
162
+ }
163
+ else {
164
+ this.star();
165
+ }
166
+ return this._starred;
167
+ }
168
+ // ==================== Hash Generation ====================
169
+ static generateHash(userID, timestamp, id, participants) {
170
+ // Normalize participants (they should already be normalized, but ensure consistency)
171
+ const normalized = MajikMessageThread.normalizeParticipants(participants);
172
+ // Join with delimiter
173
+ const combined = normalized.join("|");
174
+ const dataString = `${userID}:${timestamp.toISOString()}:${id}:${combined}`;
175
+ return sha256(dataString);
176
+ }
177
+ static generateApprovalHash(publicKey, threadID, timestamp) {
178
+ const dataString = `${publicKey}:${threadID}:${timestamp.toISOString()}`;
179
+ return sha256(dataString);
180
+ }
181
+ // ==================== Validation ====================
182
+ validate() {
183
+ try {
184
+ // Validate ID
185
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
186
+ if (!uuidRegex.test(this._id)) {
187
+ throw new ValidationError("Invalid UUID v4 format for id");
188
+ }
189
+ // Validate userID
190
+ if (!this._userID ||
191
+ typeof this._userID !== "string" ||
192
+ this._userID.trim().length === 0) {
193
+ throw new ValidationError("userID is required and must be a non-empty string");
194
+ }
195
+ // Validate owner public key
196
+ if (!this._owner ||
197
+ typeof this._owner !== "string" ||
198
+ this._owner.trim().length === 0) {
199
+ throw new ValidationError("owner public key is required and must be a non-empty string");
200
+ }
201
+ // Validate participants
202
+ if (!Array.isArray(this._participants) ||
203
+ this._participants.length === 0) {
204
+ throw new ValidationError("participants must be a non-empty array");
205
+ }
206
+ // Check if owner's public key is in participants
207
+ if (!this._participants.includes(this._owner)) {
208
+ throw new ValidationError("Owner public key must be included in participants");
209
+ }
210
+ // Validate timestamp
211
+ if (!(this._timestamp instanceof Date) ||
212
+ isNaN(this._timestamp.getTime())) {
213
+ throw new ValidationError("timestamp must be a valid Date object");
214
+ }
215
+ // Validate status
216
+ if (!Object.values(ThreadStatus).includes(this._status)) {
217
+ throw new ValidationError(`Invalid status: ${this._status}`);
218
+ }
219
+ // Validate hash
220
+ const expectedHash = MajikMessageThread.generateHash(this._userID, this._timestamp, this._id, this._participants);
221
+ if (this._hash !== expectedHash) {
222
+ throw new ValidationError("Hash mismatch - data integrity compromised");
223
+ }
224
+ // Validate deletion approvals
225
+ if (this._deletionApprovals.length > 0) {
226
+ for (const approval of this._deletionApprovals) {
227
+ // Check participant validity
228
+ if (!this._participants.includes(approval.publicKey)) {
229
+ throw new ValidationError(`Deletion approval from non-participant: ${approval.publicKey}`);
230
+ }
231
+ // Verify approval hash
232
+ const expectedHash = MajikMessageThread.generateApprovalHash(approval.publicKey, this._id, approval.timestamp);
233
+ if (approval.approvalHash !== expectedHash) {
234
+ throw new ValidationError(`Invalid approval hash for participant: ${approval.publicKey}`);
235
+ }
236
+ }
237
+ // Check for duplicate approvals
238
+ const approvedKeys = this._deletionApprovals.map((approval) => approval.publicKey);
239
+ const uniqueKeys = new Set(approvedKeys);
240
+ if (approvedKeys.length !== uniqueKeys.size) {
241
+ throw new ValidationError("Duplicate deletion approvals detected");
242
+ }
243
+ }
244
+ return true;
245
+ }
246
+ catch (error) {
247
+ if (error instanceof ValidationError) {
248
+ throw error;
249
+ }
250
+ throw new ValidationError(`Validation failed: ${error instanceof Error ? error.message : "Unknown error"}`);
251
+ }
252
+ }
253
+ // ==================== Status Management ====================
254
+ close() {
255
+ try {
256
+ if (this._status === ThreadStatus.CLOSED) {
257
+ throw new OperationNotAllowedError("Thread is already closed");
258
+ }
259
+ if (this._status === ThreadStatus.PENDING_DELETION ||
260
+ this._status === ThreadStatus.MARKED_FOR_DELETION) {
261
+ throw new OperationNotAllowedError("Cannot close a thread pending deletion");
262
+ }
263
+ this._status = ThreadStatus.CLOSED;
264
+ }
265
+ catch (error) {
266
+ if (error instanceof MajikThreadError) {
267
+ throw error;
268
+ }
269
+ throw new MajikThreadError(`Failed to close thread: ${error instanceof Error ? error.message : "Unknown error"}`, "CLOSE_FAILED");
270
+ }
271
+ }
272
+ // ==================== Deletion Approval System ====================
273
+ requestDeletion(publicKey) {
274
+ try {
275
+ // Validate public key is a participant
276
+ if (!this._participants.includes(publicKey)) {
277
+ throw new OperationNotAllowedError("Only participants can request thread deletion");
278
+ }
279
+ // Don't allow deletion requests on closed threads
280
+ if (this._status === ThreadStatus.CLOSED) {
281
+ throw new OperationNotAllowedError("Cannot request deletion of a closed thread");
282
+ }
283
+ // Check if already approved
284
+ const existingApproval = this._deletionApprovals.find((approval) => approval.publicKey === publicKey);
285
+ if (existingApproval) {
286
+ throw new OperationNotAllowedError("This participant has already approved deletion");
287
+ }
288
+ // Create approval
289
+ const timestamp = new Date();
290
+ const approvalHash = MajikMessageThread.generateApprovalHash(publicKey, this._id, timestamp);
291
+ const approval = {
292
+ publicKey,
293
+ approvalHash,
294
+ timestamp,
295
+ };
296
+ this._deletionApprovals.push(approval);
297
+ // Update status
298
+ this.updateDeletionStatus();
299
+ }
300
+ catch (error) {
301
+ if (error instanceof MajikThreadError) {
302
+ throw error;
303
+ }
304
+ throw new MajikThreadError(`Failed to request deletion: ${error instanceof Error ? error.message : "Unknown error"}`, "DELETION_REQUEST_FAILED");
305
+ }
306
+ }
307
+ updateDeletionStatus() {
308
+ if (this._deletionApprovals.length === 0) {
309
+ // No approvals - revert to non-deletion status
310
+ // Don't change status if already ONGOING or CLOSED
311
+ if (this._status === ThreadStatus.PENDING_DELETION ||
312
+ this._status === ThreadStatus.MARKED_FOR_DELETION) {
313
+ // Default to ONGOING when all approvals are revoked
314
+ this._status = ThreadStatus.ONGOING;
315
+ }
316
+ }
317
+ else if (this._deletionApprovals.length === this._participants.length) {
318
+ // All participants approved
319
+ this._status = ThreadStatus.MARKED_FOR_DELETION;
320
+ }
321
+ else {
322
+ // Partial approvals (at least 1, but not all)
323
+ this._status = ThreadStatus.PENDING_DELETION;
324
+ }
325
+ }
326
+ revokeDeletionRequest(publicKey) {
327
+ try {
328
+ const approvalIndex = this._deletionApprovals.findIndex((approval) => approval.publicKey === publicKey);
329
+ if (approvalIndex === -1) {
330
+ throw new OperationNotAllowedError("No deletion approval found for this participant");
331
+ }
332
+ this._deletionApprovals.splice(approvalIndex, 1);
333
+ this.updateDeletionStatus();
334
+ }
335
+ catch (error) {
336
+ if (error instanceof MajikThreadError) {
337
+ throw error;
338
+ }
339
+ throw new MajikThreadError(`Failed to revoke deletion request: ${error instanceof Error ? error.message : "Unknown error"}`, "REVOKE_DELETION_FAILED");
340
+ }
341
+ }
342
+ canBeDeleted() {
343
+ // First check if status allows deletion
344
+ if (this._status !== ThreadStatus.MARKED_FOR_DELETION) {
345
+ return false;
346
+ }
347
+ // Verify all participants have approved
348
+ if (this._deletionApprovals.length !== this._participants.length) {
349
+ return false;
350
+ }
351
+ // Verify each approval hash
352
+ return this.verifyDeletionApprovals();
353
+ }
354
+ getDeletionProgress() {
355
+ return {
356
+ approved: this._deletionApprovals.length,
357
+ total: this._participants.length,
358
+ percentage: (this._deletionApprovals.length / this._participants.length) * 100,
359
+ };
360
+ }
361
+ /**
362
+ * Verifies that all deletion approvals have valid hashes and all participants have approved
363
+ * @returns true if all approvals are valid and complete, false otherwise
364
+ */
365
+ verifyDeletionApprovals() {
366
+ try {
367
+ // Check if we have approvals from all participants
368
+ const approvedKeys = new Set(this._deletionApprovals.map((approval) => approval.publicKey));
369
+ // Verify all participants have approved
370
+ for (const participant of this._participants) {
371
+ if (!approvedKeys.has(participant)) {
372
+ return false;
373
+ }
374
+ }
375
+ // Verify each approval has a valid hash
376
+ for (const approval of this._deletionApprovals) {
377
+ const expectedHash = MajikMessageThread.generateApprovalHash(approval.publicKey, this._id, approval.timestamp);
378
+ if (approval.approvalHash !== expectedHash) {
379
+ // Hash mismatch - approval is invalid
380
+ return false;
381
+ }
382
+ // Ensure the public key is actually a participant
383
+ if (!this._participants.includes(approval.publicKey)) {
384
+ return false;
385
+ }
386
+ }
387
+ // Check for duplicate approvals
388
+ if (approvedKeys.size !== this._deletionApprovals.length) {
389
+ return false;
390
+ }
391
+ return true;
392
+ }
393
+ catch (error) {
394
+ // If any error occurs during verification, fail safely
395
+ return false;
396
+ }
397
+ }
398
+ /**
399
+ * Get detailed verification status of deletion approvals
400
+ * @returns Detailed information about approval validity
401
+ */
402
+ getDeletionApprovalStatus() {
403
+ const approvedKeys = this._deletionApprovals.map((approval) => approval.publicKey);
404
+ const approvedSet = new Set(approvedKeys);
405
+ const invalidApprovals = [];
406
+ const missingApprovals = [];
407
+ const duplicateApprovals = [];
408
+ // Check for duplicates
409
+ approvedKeys.forEach((key, index) => {
410
+ if (approvedKeys.indexOf(key) !== index) {
411
+ if (!duplicateApprovals.includes(key)) {
412
+ duplicateApprovals.push(key);
413
+ }
414
+ }
415
+ });
416
+ // Check for missing participants
417
+ for (const participant of this._participants) {
418
+ if (!approvedSet.has(participant)) {
419
+ missingApprovals.push(participant);
420
+ }
421
+ }
422
+ // Verify each approval hash
423
+ for (const approval of this._deletionApprovals) {
424
+ const expectedHash = MajikMessageThread.generateApprovalHash(approval.publicKey, this._id, approval.timestamp);
425
+ if (approval.approvalHash !== expectedHash) {
426
+ invalidApprovals.push(approval.publicKey);
427
+ }
428
+ // Check if approval is from non-participant
429
+ if (!this._participants.includes(approval.publicKey)) {
430
+ if (!invalidApprovals.includes(approval.publicKey)) {
431
+ invalidApprovals.push(approval.publicKey);
432
+ }
433
+ }
434
+ }
435
+ const allParticipantsApproved = missingApprovals.length === 0;
436
+ const isValid = allParticipantsApproved &&
437
+ invalidApprovals.length === 0 &&
438
+ duplicateApprovals.length === 0;
439
+ return {
440
+ isValid,
441
+ allParticipantsApproved,
442
+ invalidApprovals,
443
+ missingApprovals,
444
+ duplicateApprovals,
445
+ };
446
+ }
447
+ // ==================== Metadata Management ====================
448
+ updateMetadata(metadata) {
449
+ try {
450
+ if (this._status === ThreadStatus.MARKED_FOR_DELETION) {
451
+ throw new OperationNotAllowedError("Cannot update metadata of a thread marked for deletion");
452
+ }
453
+ this._metadata = {
454
+ ...this._metadata,
455
+ ...metadata,
456
+ lastActivity: new Date().toISOString(),
457
+ };
458
+ }
459
+ catch (error) {
460
+ if (error instanceof MajikThreadError) {
461
+ throw error;
462
+ }
463
+ throw new MajikThreadError(`Failed to update metadata: ${error instanceof Error ? error.message : "Unknown error"}`, "METADATA_UPDATE_FAILED");
464
+ }
465
+ }
466
+ // ==================== Serialization ====================
467
+ toJSON() {
468
+ return {
469
+ id: this._id,
470
+ user_id: this._userID,
471
+ owner: this._owner,
472
+ metadata: { ...this._metadata },
473
+ timestamp: this._timestamp.toISOString(),
474
+ participants: [...this._participants],
475
+ status: this._status,
476
+ hash: this._hash,
477
+ deletion_approvals: this._deletionApprovals.length > 0
478
+ ? this._deletionApprovals.map((approval) => ({
479
+ ...approval,
480
+ timestamp: approval.timestamp,
481
+ }))
482
+ : [],
483
+ starred: this._starred,
484
+ };
485
+ }
486
+ static fromJSON(json) {
487
+ try {
488
+ const data = typeof json === "string" ? JSON.parse(json) : json;
489
+ // Parse timestamp
490
+ const timestamp = new Date(data.timestamp);
491
+ if (isNaN(timestamp.getTime())) {
492
+ throw new ValidationError("Invalid timestamp in JSON data");
493
+ }
494
+ // Parse deletion approvals
495
+ const deletionApprovals = (data.deletion_approvals || []).map((approval) => ({
496
+ ...approval,
497
+ timestamp: new Date(approval.timestamp),
498
+ }));
499
+ return new MajikMessageThread(data.id, data.user_id, data.owner, data.metadata, timestamp, data.participants, data.status, data.hash, deletionApprovals, data.starred);
500
+ }
501
+ catch (error) {
502
+ if (error instanceof MajikThreadError) {
503
+ throw error;
504
+ }
505
+ throw new MajikThreadError(`Failed to parse JSON: ${error instanceof Error ? error.message : "Unknown error"}`, "JSON_PARSE_FAILED");
506
+ }
507
+ }
508
+ // ==================== Utility Methods ====================
509
+ isOwner(publicKey) {
510
+ return this._owner === publicKey;
511
+ }
512
+ isParticipant(publicKey) {
513
+ return this._participants.includes(publicKey);
514
+ }
515
+ toString() {
516
+ return JSON.stringify(this.toJSON(), null, 2);
517
+ }
518
+ /**
519
+ * Deduplicates and sorts participants to ensure consistent ordering
520
+ */
521
+ static normalizeParticipants(participants) {
522
+ const participantsSet = new Set();
523
+ participants.forEach((p) => participantsSet.add(p));
524
+ return Array.from(participantsSet).sort();
525
+ }
526
+ // ==================== Final Export Method ====================
527
+ /**
528
+ * Exports the thread with finalized metadata for analytics and archival purposes.
529
+ * This method updates metadata with actual counts and stats, sets lastActivity,
530
+ * and optionally closes the thread if no deletion is pending.
531
+ *
532
+ * @param messageCount - The actual number of messages in this thread
533
+ * @param additionalTags - Optional tags to add to existing tags
534
+ * @param autoClose - Whether to automatically close the thread (default: true)
535
+ * @returns MajikMessageThreadJSON with updated metadata
536
+ */
537
+ exportFinalStats(messageCount, additionalTags, autoClose = true) {
538
+ try {
539
+ // Validate message count
540
+ if (typeof messageCount !== "number" ||
541
+ messageCount < 0 ||
542
+ !Number.isInteger(messageCount)) {
543
+ throw new ValidationError("messageCount must be a non-negative integer");
544
+ }
545
+ // Cannot finalize a thread marked for deletion
546
+ if (this._status === ThreadStatus.MARKED_FOR_DELETION) {
547
+ throw new OperationNotAllowedError("Cannot export final stats for a thread marked for deletion");
548
+ }
549
+ // Merge tags
550
+ const existingTags = this._metadata.tags || [];
551
+ const mergedTags = additionalTags
552
+ ? Array.from(new Set([...existingTags, ...additionalTags]))
553
+ : existingTags;
554
+ // Update metadata with final stats
555
+ const finalMetadata = {
556
+ ...this._metadata,
557
+ messageCount,
558
+ lastActivity: new Date().toISOString(),
559
+ tags: mergedTags.length > 0 ? mergedTags : undefined,
560
+ };
561
+ // Determine final status
562
+ let finalStatus = this._status;
563
+ // Auto-close if requested and thread is not in deletion state
564
+ if (autoClose && this._status === ThreadStatus.ONGOING) {
565
+ finalStatus = ThreadStatus.CLOSED;
566
+ }
567
+ // If status is PENDING_DELETION, keep it as is
568
+ if (this._status === ThreadStatus.PENDING_DELETION) {
569
+ finalStatus = ThreadStatus.PENDING_DELETION;
570
+ }
571
+ // Create the export object
572
+ const exportData = {
573
+ id: this._id,
574
+ user_id: this._userID,
575
+ owner: this._owner,
576
+ metadata: finalMetadata,
577
+ timestamp: this._timestamp.toISOString(),
578
+ participants: [...this._participants],
579
+ status: finalStatus,
580
+ hash: this._hash,
581
+ deletion_approvals: this._deletionApprovals.map((approval) => ({
582
+ ...approval,
583
+ timestamp: approval.timestamp,
584
+ })),
585
+ starred: this._starred,
586
+ };
587
+ // If we're auto-closing and status changed, actually update the instance
588
+ if (autoClose &&
589
+ finalStatus === ThreadStatus.CLOSED &&
590
+ this._status === ThreadStatus.ONGOING) {
591
+ this._status = ThreadStatus.CLOSED;
592
+ this._metadata = finalMetadata;
593
+ }
594
+ else if (!autoClose) {
595
+ // Just update metadata without changing status
596
+ this._metadata = finalMetadata;
597
+ }
598
+ return exportData;
599
+ }
600
+ catch (error) {
601
+ if (error instanceof MajikThreadError) {
602
+ throw error;
603
+ }
604
+ throw new MajikThreadError(`Failed to export final stats: ${error instanceof Error ? error.message : "Unknown error"}`, "EXPORT_FINAL_STATS_FAILED");
605
+ }
606
+ }
607
+ /**
608
+ * Exports analytics-ready data for the thread
609
+ * @returns Object with analytics metadata
610
+ */
611
+ getAnalyticsData() {
612
+ const now = new Date();
613
+ const duration = now.getTime() - this._timestamp.getTime();
614
+ return {
615
+ threadID: this._id,
616
+ owner: this._owner,
617
+ userID: this._userID,
618
+ participantCount: this._participants.length,
619
+ messageCount: this._metadata.messageCount || 0,
620
+ status: this._status,
621
+ createdAt: this._timestamp.toISOString(),
622
+ lastActivity: this._metadata.lastActivity,
623
+ duration,
624
+ tags: this._metadata.tags || [],
625
+ category: this._metadata.category,
626
+ priority: this._metadata.priority,
627
+ deletionStatus: {
628
+ isPendingDeletion: this._status === ThreadStatus.PENDING_DELETION,
629
+ isMarkedForDeletion: this._status === ThreadStatus.MARKED_FOR_DELETION,
630
+ approvalProgress: (this._deletionApprovals.length / this._participants.length) * 100,
631
+ approvedCount: this._deletionApprovals.length,
632
+ totalParticipants: this._participants.length,
633
+ },
634
+ starred: this._starred,
635
+ };
636
+ }
637
+ }
@@ -2,6 +2,8 @@ export type ISODateString = string;
2
2
  export type MajikMessageAccountID = string;
3
3
  export type MajikMessagePublicKey = string;
4
4
  export type MajikMessageChatID = string;
5
+ export type MajikMessageThreadID = string;
6
+ export type MajikMessageMailID = string;
5
7
  export interface MAJIK_API_RESPONSE {
6
8
  success: boolean;
7
9
  message: string;
package/dist/index.d.ts CHANGED
@@ -15,3 +15,6 @@ export * from "./core/database/chat/majik-message-chat";
15
15
  export type * from "./core/database/chat/types";
16
16
  export * from "./core/database/system/identity";
17
17
  export * from "./core/compressor/majik-compressor";
18
+ export * from "./core/database/thread/majik-message-thread";
19
+ export * from "./core/database/thread/mail/majik-message-mail";
20
+ export * from "./core/database/thread/enums";
package/dist/index.js CHANGED
@@ -13,3 +13,6 @@ export * from "./core/utils/utilities";
13
13
  export * from "./core/database/chat/majik-message-chat";
14
14
  export * from "./core/database/system/identity";
15
15
  export * from "./core/compressor/majik-compressor";
16
+ export * from "./core/database/thread/majik-message-thread";
17
+ export * from "./core/database/thread/mail/majik-message-mail";
18
+ export * from "./core/database/thread/enums";