@majikah/majik-message 0.1.20 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ISODateString, MajikMessageAccountID, MajikMessageMailID, MajikMessagePublicKey, MajikMessageThreadID } from "../../../types";
|
|
2
2
|
import { MajikMessageIdentity } from "../../system/identity";
|
|
3
3
|
import { MajikMessageThread } from "../majik-message-thread";
|
|
4
|
+
import { FileContext, MajikFile } from "@majikah/majik-file";
|
|
4
5
|
export interface MailMetadata {
|
|
5
6
|
subject?: string;
|
|
6
|
-
attachments?:
|
|
7
|
+
attachments?: MailAttachmentRef[];
|
|
7
8
|
priority?: "low" | "medium" | "high" | "urgent";
|
|
8
9
|
labels?: string[];
|
|
9
10
|
isForwarded?: boolean;
|
|
@@ -23,6 +24,22 @@ export interface MajikMessageMailJSON {
|
|
|
23
24
|
previous_mail_id?: MajikMessageMailID;
|
|
24
25
|
read_by: MajikMessagePublicKey[];
|
|
25
26
|
}
|
|
27
|
+
export interface MailAttachmentRef {
|
|
28
|
+
/** MajikFile UUID — used to fetch the .mjkb from R2 via your file service */
|
|
29
|
+
fileId: string;
|
|
30
|
+
/** SHA-256 hex of original bytes — for dedup checks */
|
|
31
|
+
fileHash: string;
|
|
32
|
+
/** Original filename for display (e.g. "resume.pdf") */
|
|
33
|
+
originalName: string | null;
|
|
34
|
+
/** MIME type for icon/preview logic */
|
|
35
|
+
mimeType: string | null;
|
|
36
|
+
/** Original size in bytes — for "2.3 MB" display */
|
|
37
|
+
sizeOriginal: number;
|
|
38
|
+
/** R2 key — lets the Worker fetch directly without a DB lookup */
|
|
39
|
+
r2Key: string;
|
|
40
|
+
/** context from MajikFile — so the UI knows if it's a thread_attachment */
|
|
41
|
+
context: FileContext;
|
|
42
|
+
}
|
|
26
43
|
export declare class MajikMailError extends Error {
|
|
27
44
|
code: string;
|
|
28
45
|
constructor(message: string, code: string);
|
|
@@ -73,10 +90,11 @@ export declare class MajikMessageMail {
|
|
|
73
90
|
* @param message - Plain text message (encrypted)
|
|
74
91
|
* @param recipients - Array of recipient public keys (excluding sender)
|
|
75
92
|
* @param metadata - Optional mail metadata
|
|
93
|
+
* @param id - Optional ID to use
|
|
76
94
|
* @returns Promise resolving to new MajikMessageMail instance
|
|
77
95
|
* @throws Error if validation fails or thread is closed
|
|
78
96
|
*/
|
|
79
|
-
static create(thread: MajikMessageThread, identity: MajikMessageIdentity, message: string, recipients: MajikMessagePublicKey[], metadata?: MailMetadata): Promise<MajikMessageMail>;
|
|
97
|
+
static create(thread: MajikMessageThread, identity: MajikMessageIdentity, message: string, recipients: MajikMessagePublicKey[], metadata?: MailMetadata, id?: MajikMessageMailID): Promise<MajikMessageMail>;
|
|
80
98
|
/**
|
|
81
99
|
* Creates a reply to an existing mail item in the thread.
|
|
82
100
|
* Uses the previous mail's hash as part of the p_hash.
|
|
@@ -87,10 +105,30 @@ export declare class MajikMessageMail {
|
|
|
87
105
|
* @param message - Plain text message (encrypted)
|
|
88
106
|
* @param recipients - Array of recipient public keys (excluding sender)
|
|
89
107
|
* @param metadata - Optional mail metadata
|
|
108
|
+
* @param id - Optional ID to use
|
|
90
109
|
* @returns Promise resolving to new MajikMessageMail instance
|
|
91
110
|
* @throws Error if validation fails or thread is closed
|
|
92
111
|
*/
|
|
93
|
-
static reply(thread: MajikMessageThread, previousMail: MajikMessageMail, identity: MajikMessageIdentity, message: string, recipients: MajikMessagePublicKey[], metadata?: MailMetadata): Promise<MajikMessageMail>;
|
|
112
|
+
static reply(thread: MajikMessageThread, previousMail: MajikMessageMail, identity: MajikMessageIdentity, message: string, recipients: MajikMessagePublicKey[], metadata?: MailMetadata, id?: MajikMessageMailID): Promise<MajikMessageMail>;
|
|
113
|
+
/**
|
|
114
|
+
* Attach a MajikFile to this mail.
|
|
115
|
+
* Automatically wires thread_id and thread_message_id from the instance.
|
|
116
|
+
*
|
|
117
|
+
* @throws MailValidationError if the file context is not "thread_attachment"
|
|
118
|
+
* @throws MailOperationError if this file is already attached (by fileId or fileHash)
|
|
119
|
+
*/
|
|
120
|
+
attachFile(file: MajikFile): MailAttachmentRef;
|
|
121
|
+
/**
|
|
122
|
+
* Remove an attached file by fileId.
|
|
123
|
+
* Returns true if removed, false if it wasn't attached.
|
|
124
|
+
*/
|
|
125
|
+
detachFile(fileId: string): boolean;
|
|
126
|
+
/** Returns all attachment refs on this mail, typed correctly. */
|
|
127
|
+
get attachments(): MailAttachmentRef[];
|
|
128
|
+
/** Returns true if this mail has at least one attachment. */
|
|
129
|
+
get hasAttachments(): boolean;
|
|
130
|
+
/** Returns the attachment ref for a given fileId, or null if not found. */
|
|
131
|
+
getAttachment(fileId: string): MailAttachmentRef | null;
|
|
94
132
|
/**
|
|
95
133
|
* Generates the hash for the current mail item.
|
|
96
134
|
* Format: SHA256(id:message:sender:recipients:timestamp)
|
|
@@ -114,7 +152,6 @@ export declare class MajikMessageMail {
|
|
|
114
152
|
* @returns true if p_hash is valid
|
|
115
153
|
*/
|
|
116
154
|
validatePHash(previousHash: string): boolean;
|
|
117
|
-
private validateMessage;
|
|
118
155
|
/**
|
|
119
156
|
* Validates an entire chain of mail items in a thread.
|
|
120
157
|
* Verifies both hash and p_hash integrity for all items.
|
|
@@ -180,5 +217,6 @@ export declare class MajikMessageMail {
|
|
|
180
217
|
* Validates raw message length
|
|
181
218
|
*/
|
|
182
219
|
private static validateRawMessageLength;
|
|
220
|
+
private static isValidAttachmentRef;
|
|
183
221
|
private static isValidJSON;
|
|
184
222
|
}
|
|
@@ -113,10 +113,11 @@ export class MajikMessageMail {
|
|
|
113
113
|
* @param message - Plain text message (encrypted)
|
|
114
114
|
* @param recipients - Array of recipient public keys (excluding sender)
|
|
115
115
|
* @param metadata - Optional mail metadata
|
|
116
|
+
* @param id - Optional ID to use
|
|
116
117
|
* @returns Promise resolving to new MajikMessageMail instance
|
|
117
118
|
* @throws Error if validation fails or thread is closed
|
|
118
119
|
*/
|
|
119
|
-
static async create(thread, identity, message, recipients, metadata = {}) {
|
|
120
|
+
static async create(thread, identity, message, recipients, metadata = {}, id) {
|
|
120
121
|
try {
|
|
121
122
|
// Validate thread
|
|
122
123
|
if (!thread) {
|
|
@@ -175,10 +176,11 @@ export class MajikMessageMail {
|
|
|
175
176
|
// Normalize recipients (sort for consistency)
|
|
176
177
|
const normalizedRecipients = [...recipients].sort();
|
|
177
178
|
// Generate ID and timestamp
|
|
178
|
-
const
|
|
179
|
+
const generatedID = uuidv4();
|
|
180
|
+
const finalID = id || generatedID;
|
|
179
181
|
const timestamp = new Date();
|
|
180
182
|
// Generate hash for this mail item
|
|
181
|
-
const hash = this.generateHash(
|
|
183
|
+
const hash = this.generateHash(finalID, message.trim(), senderPublicKey, normalizedRecipients, timestamp);
|
|
182
184
|
// For the first item, p_hash is the thread's hash
|
|
183
185
|
const p_hash = this.generatePHash(hash, thread.hash);
|
|
184
186
|
// Mark as reply metadata
|
|
@@ -186,7 +188,7 @@ export class MajikMessageMail {
|
|
|
186
188
|
...metadata,
|
|
187
189
|
isReply: false,
|
|
188
190
|
};
|
|
189
|
-
return new MajikMessageMail(
|
|
191
|
+
return new MajikMessageMail(finalID, thread.id, accountID, message.trim(), senderPublicKey, normalizedRecipients, timestamp, finalMetadata, hash, p_hash, undefined, // No previous mail ID for first item
|
|
190
192
|
[]);
|
|
191
193
|
}
|
|
192
194
|
catch (error) {
|
|
@@ -207,10 +209,11 @@ export class MajikMessageMail {
|
|
|
207
209
|
* @param message - Plain text message (encrypted)
|
|
208
210
|
* @param recipients - Array of recipient public keys (excluding sender)
|
|
209
211
|
* @param metadata - Optional mail metadata
|
|
212
|
+
* @param id - Optional ID to use
|
|
210
213
|
* @returns Promise resolving to new MajikMessageMail instance
|
|
211
214
|
* @throws Error if validation fails or thread is closed
|
|
212
215
|
*/
|
|
213
|
-
static async reply(thread, previousMail, identity, message, recipients, metadata = {}) {
|
|
216
|
+
static async reply(thread, previousMail, identity, message, recipients, metadata = {}, id) {
|
|
214
217
|
try {
|
|
215
218
|
// Validate thread
|
|
216
219
|
if (!thread) {
|
|
@@ -279,10 +282,11 @@ export class MajikMessageMail {
|
|
|
279
282
|
// Normalize recipients (sort for consistency)
|
|
280
283
|
const normalizedRecipients = [...recipients].sort();
|
|
281
284
|
// Generate ID and timestamp
|
|
282
|
-
const
|
|
285
|
+
const generatedID = uuidv4();
|
|
286
|
+
const finalID = id || generatedID;
|
|
283
287
|
const timestamp = new Date();
|
|
284
288
|
// Generate hash for this mail item
|
|
285
|
-
const hash = this.generateHash(
|
|
289
|
+
const hash = this.generateHash(finalID, message.trim(), senderPublicKey, normalizedRecipients, timestamp);
|
|
286
290
|
// For replies, p_hash links to previous mail's hash
|
|
287
291
|
const p_hash = this.generatePHash(hash, previousMail.hash);
|
|
288
292
|
// Mark as reply in metadata
|
|
@@ -290,7 +294,7 @@ export class MajikMessageMail {
|
|
|
290
294
|
...metadata,
|
|
291
295
|
isReply: true,
|
|
292
296
|
};
|
|
293
|
-
return new MajikMessageMail(
|
|
297
|
+
return new MajikMessageMail(finalID, thread.id, accountID, message.trim(), senderPublicKey, normalizedRecipients, timestamp, finalMetadata, hash, p_hash, previousMail.id, []);
|
|
294
298
|
}
|
|
295
299
|
catch (error) {
|
|
296
300
|
if (error instanceof MajikMailError) {
|
|
@@ -299,6 +303,78 @@ export class MajikMessageMail {
|
|
|
299
303
|
throw new MailOperationError(`Failed to create reply: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
300
304
|
}
|
|
301
305
|
}
|
|
306
|
+
// In MajikMessageMail
|
|
307
|
+
/**
|
|
308
|
+
* Attach a MajikFile to this mail.
|
|
309
|
+
* Automatically wires thread_id and thread_message_id from the instance.
|
|
310
|
+
*
|
|
311
|
+
* @throws MailValidationError if the file context is not "thread_attachment"
|
|
312
|
+
* @throws MailOperationError if this file is already attached (by fileId or fileHash)
|
|
313
|
+
*/
|
|
314
|
+
attachFile(file) {
|
|
315
|
+
// Enforce context — only thread attachments belong on mail
|
|
316
|
+
if (file.context !== "thread_attachment") {
|
|
317
|
+
throw new MailValidationError(`attachFile: file must have context "thread_attachment" (got "${file.context}")`);
|
|
318
|
+
}
|
|
319
|
+
const existing = (this._metadata.attachments ?? []);
|
|
320
|
+
// Guard: duplicate by fileId
|
|
321
|
+
if (existing.some((a) => a.fileId === file.id)) {
|
|
322
|
+
throw new MailOperationError(`attachFile: file "${file.id}" is already attached to this mail`);
|
|
323
|
+
}
|
|
324
|
+
// Guard: duplicate by content hash (same file re-encrypted)
|
|
325
|
+
if (existing.some((a) => a.fileHash === file.fileHash)) {
|
|
326
|
+
throw new MailOperationError(`attachFile: a file with hash "${file.fileHash.slice(0, 8)}…" is already attached`);
|
|
327
|
+
}
|
|
328
|
+
// Wire thread context onto the file if not already bound.
|
|
329
|
+
// bindToThreadMail is a no-op guard — it throws if already set,
|
|
330
|
+
// so we only call it when both IDs are missing.
|
|
331
|
+
if (!file.threadId && !file.threadMessageId) {
|
|
332
|
+
file.bindToThreadMail(this._threadID, this._id);
|
|
333
|
+
}
|
|
334
|
+
else if (file.threadId !== this._threadID ||
|
|
335
|
+
file.threadMessageId !== this._id) {
|
|
336
|
+
// File was pre-bound to a DIFFERENT mail/thread — that's a real error
|
|
337
|
+
throw new MailOperationError(`attachFile: file "${file.id}" is already bound to a different thread/mail`);
|
|
338
|
+
}
|
|
339
|
+
const ref = {
|
|
340
|
+
fileId: file.id,
|
|
341
|
+
fileHash: file.fileHash,
|
|
342
|
+
originalName: file.originalName,
|
|
343
|
+
mimeType: file.mimeType,
|
|
344
|
+
sizeOriginal: file.sizeOriginal,
|
|
345
|
+
r2Key: file.r2Key,
|
|
346
|
+
context: file.context,
|
|
347
|
+
};
|
|
348
|
+
this._metadata = {
|
|
349
|
+
...this._metadata,
|
|
350
|
+
attachments: [...existing, ref],
|
|
351
|
+
};
|
|
352
|
+
return ref;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Remove an attached file by fileId.
|
|
356
|
+
* Returns true if removed, false if it wasn't attached.
|
|
357
|
+
*/
|
|
358
|
+
detachFile(fileId) {
|
|
359
|
+
const existing = (this._metadata.attachments ?? []);
|
|
360
|
+
const filtered = existing.filter((a) => a.fileId !== fileId);
|
|
361
|
+
if (filtered.length === existing.length)
|
|
362
|
+
return false;
|
|
363
|
+
this._metadata = { ...this._metadata, attachments: filtered };
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
/** Returns all attachment refs on this mail, typed correctly. */
|
|
367
|
+
get attachments() {
|
|
368
|
+
return [...(this._metadata.attachments ?? [])];
|
|
369
|
+
}
|
|
370
|
+
/** Returns true if this mail has at least one attachment. */
|
|
371
|
+
get hasAttachments() {
|
|
372
|
+
return ((this._metadata.attachments ?? []).length > 0);
|
|
373
|
+
}
|
|
374
|
+
/** Returns the attachment ref for a given fileId, or null if not found. */
|
|
375
|
+
getAttachment(fileId) {
|
|
376
|
+
return ((this._metadata.attachments ?? []).find((a) => a.fileId === fileId) ?? null);
|
|
377
|
+
}
|
|
302
378
|
// ==================== Hash Generation Methods ====================
|
|
303
379
|
/**
|
|
304
380
|
* Generates the hash for the current mail item.
|
|
@@ -426,11 +502,6 @@ export class MajikMessageMail {
|
|
|
426
502
|
throw new MailValidationError(`p_hash validation failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
427
503
|
}
|
|
428
504
|
}
|
|
429
|
-
validateMessage(message) {
|
|
430
|
-
if (!message || typeof message !== "string" || message.trim() === "") {
|
|
431
|
-
throw new Error("Invalid message: must be a non-empty string");
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
505
|
// ==================== Static Blockchain Validation ====================
|
|
435
506
|
/**
|
|
436
507
|
* Validates an entire chain of mail items in a thread.
|
|
@@ -691,6 +762,16 @@ export class MajikMessageMail {
|
|
|
691
762
|
if (!this.isValidJSON(data)) {
|
|
692
763
|
throw new MailValidationError("Invalid JSON: missing required fields or invalid types");
|
|
693
764
|
}
|
|
765
|
+
if (data.metadata?.attachments !== undefined) {
|
|
766
|
+
if (!Array.isArray(data.metadata.attachments)) {
|
|
767
|
+
throw new MailValidationError("Invalid JSON: metadata.attachments must be an array");
|
|
768
|
+
}
|
|
769
|
+
for (let i = 0; i < data.metadata.attachments.length; i++) {
|
|
770
|
+
if (!MajikMessageMail.isValidAttachmentRef(data.metadata.attachments[i])) {
|
|
771
|
+
throw new MailValidationError(`Invalid JSON: metadata.attachments[${i}] is not a valid MailAttachmentRef`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
694
775
|
// Parse timestamp
|
|
695
776
|
const timestamp = new Date(data.timestamp);
|
|
696
777
|
if (isNaN(timestamp.getTime())) {
|
|
@@ -725,6 +806,17 @@ export class MajikMessageMail {
|
|
|
725
806
|
`Current length: ${message.length}`);
|
|
726
807
|
}
|
|
727
808
|
}
|
|
809
|
+
static isValidAttachmentRef(a) {
|
|
810
|
+
return (a &&
|
|
811
|
+
typeof a === "object" &&
|
|
812
|
+
typeof a.fileId === "string" &&
|
|
813
|
+
typeof a.fileHash === "string" &&
|
|
814
|
+
typeof a.r2Key === "string" &&
|
|
815
|
+
typeof a.sizeOriginal === "number" &&
|
|
816
|
+
(a.originalName === null || typeof a.originalName === "string") &&
|
|
817
|
+
(a.mimeType === null || typeof a.mimeType === "string") &&
|
|
818
|
+
typeof a.context === "string");
|
|
819
|
+
}
|
|
728
820
|
static isValidJSON(json) {
|
|
729
821
|
return (json &&
|
|
730
822
|
typeof json === "object" &&
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FileContext, MajikFile, MajikFileJSON } from "@majikah/majik-file";
|
|
1
|
+
import type { FileContext, MajikFile, MajikFileJSON, TempFileDuration } from "@majikah/majik-file";
|
|
2
2
|
export type ISODateString = string;
|
|
3
3
|
export type MajikMessageAccountID = string;
|
|
4
4
|
export type MajikMessagePublicKey = string;
|
|
@@ -95,6 +95,8 @@ export interface MajikKeyMetadata {
|
|
|
95
95
|
export interface EncryptFileOptions {
|
|
96
96
|
/** Raw binary content of the file to encrypt. */
|
|
97
97
|
data: Uint8Array | ArrayBuffer;
|
|
98
|
+
/** UUID from auth.users — used for R2 key construction and ownership checks. */
|
|
99
|
+
userId?: string;
|
|
98
100
|
/**
|
|
99
101
|
* File context — drives storage routing, WebP conversion, and R2 key prefix.
|
|
100
102
|
* "user_upload" → permanent storage, no WebP conversion
|
|
@@ -125,14 +127,16 @@ export interface EncryptFileOptions {
|
|
|
125
127
|
* @default false
|
|
126
128
|
*/
|
|
127
129
|
isTemporary?: boolean;
|
|
128
|
-
/**
|
|
129
|
-
expiresAt?:
|
|
130
|
+
/** TempFileDuration in days. Required when isTemporary is true. */
|
|
131
|
+
expiresAt?: TempFileDuration;
|
|
130
132
|
/** Bypass the 100 MB file size limit. @default false */
|
|
131
133
|
bypassSizeLimit?: boolean;
|
|
132
134
|
/** Foreign-key association with a chat message. */
|
|
133
135
|
chatMessageId?: string;
|
|
134
136
|
/** Foreign-key association with a thread message. */
|
|
135
137
|
threadMessageId?: string;
|
|
138
|
+
/** Foreign-key association with a thread. */
|
|
139
|
+
threadId?: string;
|
|
136
140
|
}
|
|
137
141
|
/**
|
|
138
142
|
* Returned by MajikMessage.encryptFile().
|
package/dist/majik-message.d.ts
CHANGED
|
@@ -66,15 +66,6 @@ export declare class MajikMessage {
|
|
|
66
66
|
* @param accountId Own account ID. Defaults to the active account.
|
|
67
67
|
*/
|
|
68
68
|
private _resolveFileIdentity;
|
|
69
|
-
/**
|
|
70
|
-
* Resolve a list of contact IDs into MajikFileRecipient objects.
|
|
71
|
-
*
|
|
72
|
-
* Used for group file encryption — each recipient only needs their ML-KEM
|
|
73
|
-
* public key. Secret keys never leave their respective devices.
|
|
74
|
-
*
|
|
75
|
-
* @param ids Contact IDs from the contact directory.
|
|
76
|
-
*/
|
|
77
|
-
private _resolveFileRecipients;
|
|
78
69
|
/**
|
|
79
70
|
* Resolve a list of contact IDs into MajikFileRecipient objects.
|
|
80
71
|
*
|
|
@@ -170,25 +161,7 @@ export declare class MajikMessage {
|
|
|
170
161
|
decryptMajikMessageChat(encryptedPayload: string, recipientId?: string): Promise<string>;
|
|
171
162
|
/**
|
|
172
163
|
* Encrypt a binary file and return everything the caller needs to persist it.
|
|
173
|
-
|
|
174
|
-
* Flow:
|
|
175
|
-
* 1. Resolve the active account's full MajikFileIdentity from MajikKeyStore.
|
|
176
|
-
* 2. Resolve each recipientId into a MajikFileRecipient (public key only).
|
|
177
|
-
* MajikFile.create() silently deduplicates and strips the sender's own ID
|
|
178
|
-
* from the recipient list, so callers don't have to filter it out.
|
|
179
|
-
* 3. Delegate entirely to MajikFile.create() — it handles:
|
|
180
|
-
* • SHA-256 content hash (dedup)
|
|
181
|
-
* • WebP conversion for chat_image / chat_attachment image contexts
|
|
182
|
-
* • Zstd compression for compressible formats
|
|
183
|
-
* • ML-KEM encapsulation (single or group)
|
|
184
|
-
* • AES-256-GCM encryption
|
|
185
|
-
* • .mjkb binary encoding
|
|
186
|
-
* 4. Return the MajikFile instance, Supabase-ready metadata, and R2-ready Blob.
|
|
187
|
-
*
|
|
188
|
-
* The caller is responsible for:
|
|
189
|
-
* • Uploading `result.binary` to R2 at `result.metadata.r2_key`
|
|
190
|
-
* • Inserting `result.metadata` into the majik_files Supabase table
|
|
191
|
-
*
|
|
164
|
+
|
|
192
165
|
* @throws Error if no active account, account has no ML-KEM keys, or a
|
|
193
166
|
* recipient cannot be resolved from the contact directory.
|
|
194
167
|
* @throws MajikFileError on validation failures or crypto errors (re-thrown
|
package/dist/majik-message.js
CHANGED
|
@@ -139,36 +139,12 @@ export class MajikMessage {
|
|
|
139
139
|
`Re-import via importAccountFromMnemonicBackup() to upgrade.`);
|
|
140
140
|
}
|
|
141
141
|
return {
|
|
142
|
-
|
|
142
|
+
publicKey: key.publicKeyBase64,
|
|
143
143
|
fingerprint: key.fingerprint,
|
|
144
144
|
mlKemPublicKey: key.mlKemPublicKey,
|
|
145
145
|
mlKemSecretKey: key.getMlKemSecretKey(),
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
|
-
/**
|
|
149
|
-
* Resolve a list of contact IDs into MajikFileRecipient objects.
|
|
150
|
-
*
|
|
151
|
-
* Used for group file encryption — each recipient only needs their ML-KEM
|
|
152
|
-
* public key. Secret keys never leave their respective devices.
|
|
153
|
-
*
|
|
154
|
-
* @param ids Contact IDs from the contact directory.
|
|
155
|
-
*/
|
|
156
|
-
async _resolveFileRecipients(ids) {
|
|
157
|
-
return Promise.all(ids.map(async (id) => {
|
|
158
|
-
const contact = this.contactDirectory.getContact(id);
|
|
159
|
-
if (!contact)
|
|
160
|
-
throw new Error(`No contact found for id "${id}"`);
|
|
161
|
-
const mlPubKey = base64ToUint8Array(contact.mlKey);
|
|
162
|
-
if (!mlPubKey || mlPubKey.length === 0) {
|
|
163
|
-
throw new Error(`Contact "${id}" has no ML-KEM public key. ` +
|
|
164
|
-
`They may need to upgrade their account via importFromMnemonicBackup().`);
|
|
165
|
-
}
|
|
166
|
-
return {
|
|
167
|
-
fingerprint: contact.fingerprint,
|
|
168
|
-
mlKemPublicKey: mlPubKey,
|
|
169
|
-
};
|
|
170
|
-
}));
|
|
171
|
-
}
|
|
172
148
|
/**
|
|
173
149
|
* Resolve a list of contact IDs into MajikFileRecipient objects.
|
|
174
150
|
*
|
|
@@ -190,6 +166,7 @@ export class MajikMessage {
|
|
|
190
166
|
return {
|
|
191
167
|
fingerprint: contact.fingerprint,
|
|
192
168
|
mlKemPublicKey: mlPubKey,
|
|
169
|
+
publicKey: pkey,
|
|
193
170
|
};
|
|
194
171
|
}));
|
|
195
172
|
}
|
|
@@ -651,25 +628,7 @@ export class MajikMessage {
|
|
|
651
628
|
// ── File Encryption / Decryption ──────────────────────────────────────────
|
|
652
629
|
/**
|
|
653
630
|
* Encrypt a binary file and return everything the caller needs to persist it.
|
|
654
|
-
|
|
655
|
-
* Flow:
|
|
656
|
-
* 1. Resolve the active account's full MajikFileIdentity from MajikKeyStore.
|
|
657
|
-
* 2. Resolve each recipientId into a MajikFileRecipient (public key only).
|
|
658
|
-
* MajikFile.create() silently deduplicates and strips the sender's own ID
|
|
659
|
-
* from the recipient list, so callers don't have to filter it out.
|
|
660
|
-
* 3. Delegate entirely to MajikFile.create() — it handles:
|
|
661
|
-
* • SHA-256 content hash (dedup)
|
|
662
|
-
* • WebP conversion for chat_image / chat_attachment image contexts
|
|
663
|
-
* • Zstd compression for compressible formats
|
|
664
|
-
* • ML-KEM encapsulation (single or group)
|
|
665
|
-
* • AES-256-GCM encryption
|
|
666
|
-
* • .mjkb binary encoding
|
|
667
|
-
* 4. Return the MajikFile instance, Supabase-ready metadata, and R2-ready Blob.
|
|
668
|
-
*
|
|
669
|
-
* The caller is responsible for:
|
|
670
|
-
* • Uploading `result.binary` to R2 at `result.metadata.r2_key`
|
|
671
|
-
* • Inserting `result.metadata` into the majik_files Supabase table
|
|
672
|
-
*
|
|
631
|
+
|
|
673
632
|
* @throws Error if no active account, account has no ML-KEM keys, or a
|
|
674
633
|
* recipient cannot be resolved from the contact directory.
|
|
675
634
|
* @throws MajikFileError on validation failures or crypto errors (re-thrown
|
|
@@ -700,10 +659,11 @@ export class MajikMessage {
|
|
|
700
659
|
* ```
|
|
701
660
|
*/
|
|
702
661
|
async encryptFile(options) {
|
|
703
|
-
const { data, context, originalName, mimeType, recipients = [], conversationId, isTemporary = false, expiresAt, bypassSizeLimit = false, chatMessageId, threadMessageId, } = options;
|
|
662
|
+
const { data, context, originalName, mimeType, recipients = [], conversationId, isTemporary = false, expiresAt, bypassSizeLimit = false, chatMessageId, threadMessageId, threadId, userId, } = options;
|
|
704
663
|
// ── 1. Resolve sender identity ──────────────────────────────────────────
|
|
705
664
|
// Builds MajikFileIdentity with both public + secret keys from keystore.
|
|
706
665
|
const identity = await this._resolveFileIdentity();
|
|
666
|
+
const finalUserID = userId ?? identity.publicKey;
|
|
707
667
|
// ── 2. Resolve additional recipients ───────────────────────────────────
|
|
708
668
|
// MajikFile.create() will silently drop the sender's own fingerprint if
|
|
709
669
|
// it appears in this list, and will deduplicate any repeated entries.
|
|
@@ -725,6 +685,8 @@ export class MajikMessage {
|
|
|
725
685
|
bypassSizeLimit,
|
|
726
686
|
chatMessageId,
|
|
727
687
|
threadMessageId,
|
|
688
|
+
userId: finalUserID,
|
|
689
|
+
threadId: threadId,
|
|
728
690
|
});
|
|
729
691
|
// ── 4. Package the result ───────────────────────────────────────────────
|
|
730
692
|
return {
|
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.
|
|
5
|
+
"version": "0.2.0",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"author": "Zelijah",
|
|
8
8
|
"main": "./dist/index.js",
|
|
@@ -81,8 +81,8 @@
|
|
|
81
81
|
"dependencies": {
|
|
82
82
|
"@bokuweb/zstd-wasm": "^0.0.27",
|
|
83
83
|
"@majikah/majik-envelope": "^0.0.1",
|
|
84
|
-
"@majikah/majik-file": "^0.0.
|
|
85
|
-
"@majikah/majik-key": "^0.1.
|
|
84
|
+
"@majikah/majik-file": "^0.0.13",
|
|
85
|
+
"@majikah/majik-key": "^0.1.9",
|
|
86
86
|
"@noble/hashes": "^2.0.1",
|
|
87
87
|
"@noble/post-quantum": "^0.5.4",
|
|
88
88
|
"@scure/bip39": "^1.6.0",
|