@majikah/majik-message 0.2.2 → 0.2.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.
@@ -51,7 +51,6 @@ export declare function deriveKeyFromPassphrase(passphrase: string, salt: Uint8A
51
51
  */
52
52
  export declare function deriveKeyFromMnemonic(mnemonic: string, salt: Uint8Array, iterations?: number, keyLen?: number): Uint8Array;
53
53
  export declare function x25519SharedSecret(privRaw: Uint8Array, pubRaw: Uint8Array): Uint8Array;
54
- export declare function sha256(input: string): string;
55
54
  /**
56
55
  * Derive a deterministic ML-KEM-768 keypair from a BIP-39 mnemonic seed.
57
56
  *
@@ -116,3 +115,5 @@ export declare function mlKemEncapsulate(recipientPublicKey: Uint8Array): {
116
115
  * @returns sharedSecret (32 bytes)
117
116
  */
118
117
  export declare function mlKemDecapsulate(cipherText: Uint8Array, recipientSecretKey: Uint8Array): Uint8Array;
118
+ export declare function sha256(input: string): string;
119
+ export declare function sha512(input: string): string;
@@ -10,6 +10,8 @@ import { arrayToBase64 } from "../utils/utilities";
10
10
  import { argon2id } from "@noble/hashes/argon2.js";
11
11
  import { ARGON2_PARAMS } from "./constants";
12
12
  import { ml_kem768 } from "@noble/post-quantum/ml-kem.js";
13
+ import { sha3_512 } from "@noble/hashes/sha3.js";
14
+ import { bytesToHex } from "@noble/hashes/utils.js";
13
15
  export const IV_LENGTH = 12;
14
16
  export function generateRandomBytes(len) {
15
17
  const b = new Uint8Array(len);
@@ -113,10 +115,6 @@ export function x25519SharedSecret(privRaw, pubRaw) {
113
115
  }
114
116
  throw new Error("@stablelib/x25519: compatible API not found");
115
117
  }
116
- export function sha256(input) {
117
- const hashed = hash(new TextEncoder().encode(input));
118
- return arrayToBase64(hashed);
119
- }
120
118
  // ─── ML-KEM-768: Post-Quantum Key Encapsulation ───────────────────────────────
121
119
  /**
122
120
  * Derive a deterministic ML-KEM-768 keypair from a BIP-39 mnemonic seed.
@@ -185,3 +183,12 @@ export function mlKemEncapsulate(recipientPublicKey) {
185
183
  export function mlKemDecapsulate(cipherText, recipientSecretKey) {
186
184
  return ml_kem768.decapsulate(cipherText, recipientSecretKey);
187
185
  }
186
+ // ─── Hashing ───────────────────────────────
187
+ export function sha256(input) {
188
+ const hashed = hash(new TextEncoder().encode(input));
189
+ return arrayToBase64(hashed);
190
+ }
191
+ export function sha512(input) {
192
+ const hashed = sha3_512(new TextEncoder().encode(input));
193
+ return bytesToHex(hashed);
194
+ }
@@ -62,9 +62,28 @@ export interface MajikMessageThreadJSON {
62
62
  participants: string[];
63
63
  status: ThreadStatus;
64
64
  hash: string;
65
+ t_hash: string | null;
65
66
  deletion_approvals: DeletionApproval[];
66
67
  starred: boolean;
67
68
  }
69
+ export interface MajikMessageThreadExport {
70
+ thread: MajikMessageThreadJSON;
71
+ messages: MajikMessageMailJSON[];
72
+ exported_at: ISODateString;
73
+ message_count: number;
74
+ }
75
+ /**
76
+ * Result shape returned by MajikMessageThread.auditThread().
77
+ * isValid is true only when ALL three checks pass.
78
+ */
79
+ export interface ThreadAuditResult {
80
+ isValid: boolean;
81
+ threadValid: boolean;
82
+ chainValid: boolean;
83
+ hashValid: boolean;
84
+ errors: string[];
85
+ tamperedMailIDs: string[];
86
+ }
68
87
  export declare class MajikThreadError extends Error {
69
88
  code: string;
70
89
  constructor(message: string, code: string);
@@ -84,6 +103,7 @@ export declare class MajikMessageThread {
84
103
  private readonly _participants;
85
104
  private _status;
86
105
  private readonly _hash;
106
+ private _thash;
87
107
  private _deletionApprovals;
88
108
  private _starred;
89
109
  private constructor();
@@ -95,9 +115,83 @@ export declare class MajikMessageThread {
95
115
  get participants(): readonly MajikMessagePublicKey[];
96
116
  get status(): ThreadStatus;
97
117
  get hash(): string;
118
+ get threadHash(): string | null;
98
119
  get deletionApprovals(): readonly DeletionApproval[];
99
120
  get starred(): boolean;
121
+ set subject(value: string | undefined);
100
122
  static create(userID: MajikUserID, owner: MajikMessageIdentity, participants: MajikMessagePublicKey[], metadata?: ThreadMetadata): MajikMessageThread;
123
+ /**
124
+ * Computes and stamps the t_hash for this thread.
125
+ *
126
+ * The t_hash is a SHA3-512 fingerprint of the entire thread's message history
127
+ * combined with the thread's own identity hash. It acts as a tamper-evident
128
+ * seal over the full conversation — if any message is altered, the t_hash
129
+ * won't match.
130
+ *
131
+ * Input string layout (joined with ":"):
132
+ * <thread.hash> : <mail[0].hash> : <mail[1].hash> : … : <thread.id>
133
+ * where mails are sorted by timestamp ascending (oldest first).
134
+ *
135
+ * Calling this with an empty array is valid — it seals a thread that has no
136
+ * messages yet (useful for archival of zero-message threads).
137
+ *
138
+ * @param mails - The full list of MajikMessageMailJSON for this thread.
139
+ * Does not need to be pre-sorted.
140
+ * @returns The updated thread instance (for chaining).
141
+ */
142
+ generateThreadHash(mails: MajikMessageMailJSON[]): this;
143
+ /**
144
+ * Verifies that the stored t_hash matches a freshly computed one.
145
+ *
146
+ * @param mails - The full list of MajikMessageMailJSON for this thread.
147
+ * @returns true if the t_hash is valid and matches, false if t_hash not yet set.
148
+ * @throws ValidationError if the computed hash does not match the stored one.
149
+ */
150
+ verifyThreadHash(mails: MajikMessageMailJSON[]): boolean;
151
+ /**
152
+ * Performs a full forensic audit of the thread.
153
+ *
154
+ * Accepts raw MajikMessageMailJSON — instances are hydrated internally via
155
+ * MajikMessageMail.fromJSON so the call site only needs one array.
156
+ *
157
+ * Three checks must all pass for `isValid` to be true:
158
+ *
159
+ * 1. **threadValid** — `thread.validate()` passes (structural integrity of the
160
+ * thread object itself: UUID, hash, participants, deletion approvals, etc.)
161
+ *
162
+ * 2. **chainValid** — `MajikMessageMail.validateMailChain()` passes (every
163
+ * message hash is self-consistent and the blockchain p_hash linkage from
164
+ * thread → mail[0] → mail[1] → … is intact).
165
+ *
166
+ * 3. **hashValid** — `thread.verifyThreadHash()` passes (the SHA3-512 t_hash
167
+ * sealed over the full message list still matches). Returns false — not an
168
+ * error — when t_hash has not yet been generated, meaning the thread is
169
+ * structurally sound but still unsealed.
170
+ *
171
+ * @param thread - The thread instance to audit.
172
+ * @param mailJSONs - Full list of MajikMessageMailJSON for this thread.
173
+ * Does not need to be pre-sorted.
174
+ * @returns ThreadAuditResult with granular pass/fail per check plus error details.
175
+ */
176
+ static auditThread(thread: MajikMessageThread, mailJSONs: MajikMessageMailJSON[]): ThreadAuditResult;
177
+ /**
178
+ * Exports the thread and its full message history as a self-contained snapshot.
179
+ *
180
+ * Requires that `generateThreadHash()` has already been called — the t_hash is
181
+ * the integrity seal over the exported payload and must be present before the
182
+ * export is considered authoritative. Call `generateThreadHash(mails)` first if
183
+ * it has not been set yet.
184
+ *
185
+ * The returned messages are sorted by timestamp ascending (oldest first) so the
186
+ * export is always deterministic regardless of the order they were passed in.
187
+ *
188
+ * @param mails - The full list of MajikMessageMailJSON for this thread.
189
+ * @returns MajikMessageThreadExport containing the sealed thread JSON, sorted
190
+ * messages, export timestamp, and message count.
191
+ * @throws OperationNotAllowedError if t_hash has not been generated yet.
192
+ * @throws ValidationError if any mail does not belong to this thread.
193
+ */
194
+ exportThread(mails: MajikMessageMailJSON[]): MajikMessageThreadExport;
101
195
  /**
102
196
  * Stars the thread for the user
103
197
  */
@@ -1,6 +1,7 @@
1
1
  import { v4 as uuidv4 } from "uuid";
2
2
  import { ThreadStatus } from "./enums";
3
- import { sha256 } from "../../crypto/crypto-provider";
3
+ import { sha256, sha512 } from "../../crypto/crypto-provider";
4
+ import { MajikMessageMail, } from "./mail/majik-message-mail";
4
5
  // ==================== Custom Errors ====================
5
6
  export class MajikThreadError extends Error {
6
7
  code;
@@ -22,6 +23,38 @@ export class OperationNotAllowedError extends MajikThreadError {
22
23
  this.name = "OperationNotAllowedError";
23
24
  }
24
25
  }
26
+ /**
27
+ * Sorts an array of MajikMessageMailJSON by timestamp (oldest → newest).
28
+ * Returns a new array — does not mutate the input.
29
+ */
30
+ function sortMailsByTimestamp(mails) {
31
+ return [...mails].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
32
+ }
33
+ /**
34
+ * Joins an array of strings with ":" as the delimiter.
35
+ *
36
+ * joinWithColon(["abc", "def", "ghi"]) → "abc:def:ghi"
37
+ */
38
+ function joinWithColon(parts) {
39
+ return parts.join(":");
40
+ }
41
+ /**
42
+ * Builds the raw string fed into SHA3-512 for t_hash.
43
+ *
44
+ * Layout: <thread.hash> : <mail[0].hash> : … : <mail[n].hash> : <thread.id>
45
+ *
46
+ * @param threadHash - The thread's own SHA-256 hash
47
+ * @param threadID - The thread's UUID
48
+ * @param sortedMails - Messages pre-sorted by timestamp ascending
49
+ */
50
+ function buildThreadHashInput(threadHash, threadID, sortedMails) {
51
+ const parts = [
52
+ threadHash,
53
+ ...sortedMails.map((m) => m.hash),
54
+ threadID,
55
+ ];
56
+ return joinWithColon(parts);
57
+ }
25
58
  // ==================== Main Class ====================
26
59
  export class MajikMessageThread {
27
60
  _id;
@@ -32,10 +65,11 @@ export class MajikMessageThread {
32
65
  _participants;
33
66
  _status;
34
67
  _hash;
68
+ _thash;
35
69
  _deletionApprovals;
36
70
  _starred;
37
71
  // ==================== Private Constructor ====================
38
- constructor(id, userID, owner, metadata, timestamp, participants, status, hash, deletionApprovals = [], starred = false) {
72
+ constructor(id, userID, owner, metadata, timestamp, participants, status, hash, deletionApprovals = [], starred = false, thash = null) {
39
73
  this._id = id;
40
74
  this._userID = userID;
41
75
  this._owner = owner;
@@ -44,6 +78,7 @@ export class MajikMessageThread {
44
78
  this._participants = participants;
45
79
  this._status = status;
46
80
  this._hash = hash;
81
+ this._thash = thash;
47
82
  this._deletionApprovals = deletionApprovals;
48
83
  this._starred = starred;
49
84
  // Validate on construction
@@ -74,12 +109,18 @@ export class MajikMessageThread {
74
109
  get hash() {
75
110
  return this._hash;
76
111
  }
112
+ get threadHash() {
113
+ return this._thash;
114
+ }
77
115
  get deletionApprovals() {
78
116
  return [...this._deletionApprovals];
79
117
  }
80
118
  get starred() {
81
119
  return this._starred;
82
120
  }
121
+ set subject(value) {
122
+ this.updateMetadata({ subject: value });
123
+ }
83
124
  // ==================== Static Create Method ====================
84
125
  static create(userID, owner, participants, metadata = {}) {
85
126
  try {
@@ -108,7 +149,7 @@ export class MajikMessageThread {
108
149
  const status = ThreadStatus.ONGOING;
109
150
  // Generate hash
110
151
  const hash = MajikMessageThread.generateHash(userID, timestamp, id, uniqueParticipants);
111
- return new MajikMessageThread(id, userID, owner.id, metadata, timestamp, uniqueParticipants, status, hash, []);
152
+ return new MajikMessageThread(id, userID, owner.id, metadata, timestamp, uniqueParticipants, status, hash, [], false, null);
112
153
  }
113
154
  catch (error) {
114
155
  if (error instanceof MajikThreadError) {
@@ -117,6 +158,210 @@ export class MajikMessageThread {
117
158
  throw new MajikThreadError(`Failed to create MajikMessageThread: ${error instanceof Error ? error.message : "Unknown error"}`, "CREATE_FAILED");
118
159
  }
119
160
  }
161
+ // ==================== Thread Hash (t_hash) ====================
162
+ /**
163
+ * Computes and stamps the t_hash for this thread.
164
+ *
165
+ * The t_hash is a SHA3-512 fingerprint of the entire thread's message history
166
+ * combined with the thread's own identity hash. It acts as a tamper-evident
167
+ * seal over the full conversation — if any message is altered, the t_hash
168
+ * won't match.
169
+ *
170
+ * Input string layout (joined with ":"):
171
+ * <thread.hash> : <mail[0].hash> : <mail[1].hash> : … : <thread.id>
172
+ * where mails are sorted by timestamp ascending (oldest first).
173
+ *
174
+ * Calling this with an empty array is valid — it seals a thread that has no
175
+ * messages yet (useful for archival of zero-message threads).
176
+ *
177
+ * @param mails - The full list of MajikMessageMailJSON for this thread.
178
+ * Does not need to be pre-sorted.
179
+ * @returns The updated thread instance (for chaining).
180
+ */
181
+ generateThreadHash(mails) {
182
+ try {
183
+ // ── One-time seal guard ────────────────────────────────────────────────
184
+ // t_hash is write-once. Once stamped it represents a finalized snapshot
185
+ // of the conversation. If you need to re-seal (e.g. after more messages),
186
+ // reconstruct the thread via fromJSON with t_hash: null first.
187
+ if (this._thash !== null) {
188
+ throw new OperationNotAllowedError("Thread hash has already been generated. t_hash is write-once and cannot be overwritten.");
189
+ }
190
+ if (!Array.isArray(mails)) {
191
+ throw new ValidationError("mails must be an array");
192
+ }
193
+ // Validate every mail belongs to this thread
194
+ for (const mail of mails) {
195
+ if (mail.thread_id !== this._id) {
196
+ throw new ValidationError(`Mail "${mail.id}" belongs to thread "${mail.thread_id}", not "${this._id}"`);
197
+ }
198
+ }
199
+ const sorted = sortMailsByTimestamp(mails);
200
+ const input = buildThreadHashInput(this._hash, this._id, sorted);
201
+ this._thash = sha512(input);
202
+ return this;
203
+ }
204
+ catch (error) {
205
+ if (error instanceof MajikThreadError) {
206
+ throw error;
207
+ }
208
+ throw new MajikThreadError(`Failed to generate thread hash: ${error instanceof Error ? error.message : "Unknown error"}`, "THREAD_HASH_FAILED");
209
+ }
210
+ }
211
+ /**
212
+ * Verifies that the stored t_hash matches a freshly computed one.
213
+ *
214
+ * @param mails - The full list of MajikMessageMailJSON for this thread.
215
+ * @returns true if the t_hash is valid and matches, false if t_hash not yet set.
216
+ * @throws ValidationError if the computed hash does not match the stored one.
217
+ */
218
+ verifyThreadHash(mails) {
219
+ if (this._thash === null) {
220
+ return false; // Not yet generated — nothing to verify
221
+ }
222
+ if (!Array.isArray(mails)) {
223
+ throw new ValidationError("mails must be an array");
224
+ }
225
+ const sorted = sortMailsByTimestamp(mails);
226
+ const input = buildThreadHashInput(this._hash, this._id, sorted);
227
+ const expected = sha512(input);
228
+ if (this._thash !== expected) {
229
+ throw new ValidationError("Thread hash (t_hash) mismatch — message history integrity compromised");
230
+ }
231
+ return true;
232
+ }
233
+ // ==================== Full Thread Audit ====================
234
+ /**
235
+ * Performs a full forensic audit of the thread.
236
+ *
237
+ * Accepts raw MajikMessageMailJSON — instances are hydrated internally via
238
+ * MajikMessageMail.fromJSON so the call site only needs one array.
239
+ *
240
+ * Three checks must all pass for `isValid` to be true:
241
+ *
242
+ * 1. **threadValid** — `thread.validate()` passes (structural integrity of the
243
+ * thread object itself: UUID, hash, participants, deletion approvals, etc.)
244
+ *
245
+ * 2. **chainValid** — `MajikMessageMail.validateMailChain()` passes (every
246
+ * message hash is self-consistent and the blockchain p_hash linkage from
247
+ * thread → mail[0] → mail[1] → … is intact).
248
+ *
249
+ * 3. **hashValid** — `thread.verifyThreadHash()` passes (the SHA3-512 t_hash
250
+ * sealed over the full message list still matches). Returns false — not an
251
+ * error — when t_hash has not yet been generated, meaning the thread is
252
+ * structurally sound but still unsealed.
253
+ *
254
+ * @param thread - The thread instance to audit.
255
+ * @param mailJSONs - Full list of MajikMessageMailJSON for this thread.
256
+ * Does not need to be pre-sorted.
257
+ * @returns ThreadAuditResult with granular pass/fail per check plus error details.
258
+ */
259
+ static auditThread(thread, mailJSONs) {
260
+ const errors = [];
261
+ let tamperedMailIDs = [];
262
+ let threadValid = false;
263
+ let chainValid = false;
264
+ let hashValid = false;
265
+ // ── 1. Structural thread validation ───────────────────────────────────────
266
+ try {
267
+ thread.validate();
268
+ threadValid = true;
269
+ }
270
+ catch (err) {
271
+ errors.push(`Thread structure invalid: ${err instanceof Error ? err.message : "Unknown error"}`);
272
+ }
273
+ // ── 2. Hydrate JSON → instances, then run chain validation ────────────────
274
+ // bypassValidation=false so fromJSON still runs per-mail validation.
275
+ // If any individual mail fails to parse we record the error and skip the
276
+ // chain check rather than throwing out of the audit entirely.
277
+ let mailInstances = [];
278
+ try {
279
+ mailInstances = mailJSONs.map((json) => MajikMessageMail.fromJSON(json));
280
+ }
281
+ catch (err) {
282
+ errors.push(`Failed to hydrate mail JSON: ${err instanceof Error ? err.message : "Unknown error"}`);
283
+ }
284
+ // Only run chain validation when hydration succeeded (or the list was empty)
285
+ if (mailInstances.length === mailJSONs.length) {
286
+ try {
287
+ const chainResult = MajikMessageMail.validateMailChain(thread, mailInstances);
288
+ chainValid = chainResult.isValid;
289
+ if (!chainResult.isValid) {
290
+ errors.push(...chainResult.errors);
291
+ tamperedMailIDs = chainResult.tamperedItems;
292
+ }
293
+ }
294
+ catch (err) {
295
+ errors.push(`Mail chain audit threw unexpectedly: ${err instanceof Error ? err.message : "Unknown error"}`);
296
+ }
297
+ }
298
+ // ── 3. Thread hash (t_hash) verification ──────────────────────────────────
299
+ // At this point t_hash is guaranteed non-null (the early guard above threw
300
+ // otherwise). verifyThreadHash only throws on an actual mismatch.
301
+ try {
302
+ hashValid = thread.verifyThreadHash(mailJSONs);
303
+ }
304
+ catch (err) {
305
+ errors.push(`Thread hash mismatch: ${err instanceof Error ? err.message : "Unknown error"}`);
306
+ }
307
+ return {
308
+ isValid: threadValid && chainValid && hashValid,
309
+ threadValid,
310
+ chainValid,
311
+ hashValid,
312
+ errors,
313
+ tamperedMailIDs: Array.from(new Set(tamperedMailIDs)),
314
+ };
315
+ }
316
+ // ==================== Thread Export ====================
317
+ /**
318
+ * Exports the thread and its full message history as a self-contained snapshot.
319
+ *
320
+ * Requires that `generateThreadHash()` has already been called — the t_hash is
321
+ * the integrity seal over the exported payload and must be present before the
322
+ * export is considered authoritative. Call `generateThreadHash(mails)` first if
323
+ * it has not been set yet.
324
+ *
325
+ * The returned messages are sorted by timestamp ascending (oldest first) so the
326
+ * export is always deterministic regardless of the order they were passed in.
327
+ *
328
+ * @param mails - The full list of MajikMessageMailJSON for this thread.
329
+ * @returns MajikMessageThreadExport containing the sealed thread JSON, sorted
330
+ * messages, export timestamp, and message count.
331
+ * @throws OperationNotAllowedError if t_hash has not been generated yet.
332
+ * @throws ValidationError if any mail does not belong to this thread.
333
+ */
334
+ exportThread(mails) {
335
+ try {
336
+ // t_hash must be set — an unsealed thread cannot be exported
337
+ if (this._thash === null) {
338
+ throw new OperationNotAllowedError("Cannot export thread: t_hash has not been generated yet. " +
339
+ "Call generateThreadHash(mails) before exporting.");
340
+ }
341
+ if (!Array.isArray(mails)) {
342
+ throw new ValidationError("mails must be an array");
343
+ }
344
+ // Validate every mail belongs to this thread
345
+ for (const mail of mails) {
346
+ if (mail.thread_id !== this._id) {
347
+ throw new ValidationError(`Mail "${mail.id}" belongs to thread "${mail.thread_id}", not "${this._id}"`);
348
+ }
349
+ }
350
+ const sortedMails = sortMailsByTimestamp(mails);
351
+ return {
352
+ thread: this.toJSON(),
353
+ messages: sortedMails,
354
+ exported_at: new Date().toISOString(),
355
+ message_count: sortedMails.length,
356
+ };
357
+ }
358
+ catch (error) {
359
+ if (error instanceof MajikThreadError) {
360
+ throw error;
361
+ }
362
+ throw new MajikThreadError(`Failed to export thread: ${error instanceof Error ? error.message : "Unknown error"}`, "EXPORT_THREAD_FAILED");
363
+ }
364
+ }
120
365
  // ==================== Star Management ====================
121
366
  /**
122
367
  * Stars the thread for the user
@@ -225,8 +470,8 @@ export class MajikMessageThread {
225
470
  throw new ValidationError(`Deletion approval from non-participant: ${approval.publicKey}`);
226
471
  }
227
472
  // Verify approval hash
228
- const expectedHash = MajikMessageThread.generateApprovalHash(approval.publicKey, this._id, approval.timestamp);
229
- if (approval.approvalHash !== expectedHash) {
473
+ const expectedApprovalHash = MajikMessageThread.generateApprovalHash(approval.publicKey, this._id, approval.timestamp);
474
+ if (approval.approvalHash !== expectedApprovalHash) {
230
475
  throw new ValidationError(`Invalid approval hash for participant: ${approval.publicKey}`);
231
476
  }
232
477
  }
@@ -474,6 +719,7 @@ export class MajikMessageThread {
474
719
  participants: [...this._participants],
475
720
  status: this._status,
476
721
  hash: this._hash,
722
+ t_hash: this._thash,
477
723
  deletion_approvals: this._deletionApprovals.length > 0
478
724
  ? this._deletionApprovals.map((approval) => ({
479
725
  ...approval,
@@ -496,7 +742,7 @@ export class MajikMessageThread {
496
742
  ...approval,
497
743
  timestamp: new Date(approval.timestamp),
498
744
  }));
499
- return new MajikMessageThread(data.id, data.user_id, data.owner, data.metadata, timestamp, data.participants, data.status, data.hash, deletionApprovals, data.starred);
745
+ return new MajikMessageThread(data.id, data.user_id, data.owner, data.metadata, timestamp, data.participants, data.status, data.hash, deletionApprovals, data.starred, data.t_hash);
500
746
  }
501
747
  catch (error) {
502
748
  if (error instanceof MajikThreadError) {
@@ -581,6 +827,7 @@ export class MajikMessageThread {
581
827
  participants: [...this._participants],
582
828
  status: finalStatus,
583
829
  hash: this._hash,
830
+ t_hash: this._thash,
584
831
  deletion_approvals: this._deletionApprovals.map((approval) => ({
585
832
  ...approval,
586
833
  timestamp: approval.timestamp,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@majikah/majik-message",
3
3
  "type": "module",
4
4
  "description": "Post-quantum end-to-end encryption with ML-KEM-768. Seed phrase–based accounts. Auto-expiring messages. Offline-ready. Exportable encrypted messages. Tamper-proof threads with blockchain-like integrity. Quantum-resistant messaging.",
5
- "version": "0.2.2",
5
+ "version": "0.2.3",
6
6
  "license": "Apache-2.0",
7
7
  "author": "Zelijah",
8
8
  "main": "./dist/index.js",