@thezelijah/majik-message 1.0.16 → 1.0.17

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.
@@ -24,11 +24,14 @@ export declare class MajikContactDirectory {
24
24
  */
25
25
  getContactByPublicKeyBase64(publicKeyBase64: string): Promise<MajikContact | undefined>;
26
26
  hasFingerprint(fingerprint: string): boolean;
27
- listContacts(sortedByLabel?: boolean): MajikContact[];
27
+ listContacts(sortedByLabel?: boolean, majikahOnly?: boolean): MajikContact[];
28
28
  blockContact(id: string): MajikContact;
29
29
  unblockContact(id: string): MajikContact;
30
30
  hasContact(id: string): boolean;
31
31
  clear(): this;
32
+ setMajikahStatus(id: string, status: boolean): MajikContact;
33
+ isMajikahIdentityChecked(id: string): boolean;
34
+ isMajikahRegistered(id: string): boolean;
32
35
  /**
33
36
  * Checks if a given envelope corresponds to a known contact
34
37
  */
@@ -100,8 +100,11 @@ export class MajikContactDirectory {
100
100
  hasFingerprint(fingerprint) {
101
101
  return this.fingerprintMap.has(fingerprint);
102
102
  }
103
- listContacts(sortedByLabel = false) {
104
- const contacts = [...this.contacts.values()];
103
+ listContacts(sortedByLabel = false, majikahOnly = false) {
104
+ let contacts = [...this.contacts.values()];
105
+ if (majikahOnly) {
106
+ contacts = contacts.filter((c) => c.isMajikahRegistered());
107
+ }
105
108
  if (sortedByLabel) {
106
109
  contacts.sort((a, b) => (a.meta.label || "").localeCompare(b.meta.label || ""));
107
110
  }
@@ -127,6 +130,25 @@ export class MajikContactDirectory {
127
130
  this.fingerprintMap.clear();
128
131
  return this;
129
132
  }
133
+ setMajikahStatus(id, status) {
134
+ const contact = this.getContact(id);
135
+ if (!contact)
136
+ throw new MajikContactDirectoryError("Contact not found");
137
+ contact.setMajikahStatus(status);
138
+ return contact;
139
+ }
140
+ isMajikahIdentityChecked(id) {
141
+ const contact = this.getContact(id);
142
+ if (!contact)
143
+ throw new MajikContactDirectoryError("Contact not found");
144
+ return contact.isMajikahIdentityChecked();
145
+ }
146
+ isMajikahRegistered(id) {
147
+ const contact = this.getContact(id);
148
+ if (!contact)
149
+ throw new MajikContactDirectoryError("Contact not found");
150
+ return contact.isMajikahRegistered();
151
+ }
130
152
  /**
131
153
  * Checks if a given envelope corresponds to a known contact
132
154
  */
@@ -17,6 +17,7 @@ export interface MajikContactData {
17
17
  };
18
18
  fingerprint: string;
19
19
  meta?: MajikContactMeta;
20
+ majikah_registered?: boolean;
20
21
  }
21
22
  export interface MajikContactCard {
22
23
  id: string;
@@ -35,6 +36,7 @@ export declare class MajikContact {
35
36
  };
36
37
  readonly fingerprint: string;
37
38
  meta: MajikContactMeta;
39
+ private majikah_registered?;
38
40
  constructor(data: MajikContactData);
39
41
  static create(id: string, publicKey: CryptoKey | {
40
42
  raw: Uint8Array;
@@ -49,6 +51,9 @@ export declare class MajikContact {
49
51
  setBlocked(blocked: boolean): this;
50
52
  block(): this;
51
53
  unblock(): this;
54
+ isMajikahIdentityChecked(): boolean;
55
+ isMajikahRegistered(): boolean;
56
+ setMajikahStatus(status: boolean): this;
52
57
  /**
53
58
  * Support both CryptoKey and raw-key wrappers (fallbacks when WebCrypto X25519 unsupported)
54
59
  */
@@ -18,6 +18,7 @@ export class MajikContact {
18
18
  publicKey;
19
19
  fingerprint;
20
20
  meta;
21
+ majikah_registered;
21
22
  constructor(data) {
22
23
  this.assertId(data.id);
23
24
  this.assertPublicKey(data.publicKey);
@@ -103,6 +104,16 @@ export class MajikContact {
103
104
  this.setBlocked(false);
104
105
  return this;
105
106
  }
107
+ isMajikahIdentityChecked() {
108
+ return this.majikah_registered !== undefined;
109
+ }
110
+ isMajikahRegistered() {
111
+ return this.majikah_registered || false;
112
+ }
113
+ setMajikahStatus(status) {
114
+ this.majikah_registered = status;
115
+ return this;
116
+ }
106
117
  /**
107
118
  * Support both CryptoKey and raw-key wrappers (fallbacks when WebCrypto X25519 unsupported)
108
119
  */
@@ -127,6 +138,7 @@ export class MajikContact {
127
138
  fingerprint: this.fingerprint,
128
139
  meta: { ...this.meta },
129
140
  publicKeyBase64: await this.getPublicKeyBase64(),
141
+ majikah_registered: this.majikah_registered,
130
142
  };
131
143
  }
132
144
  /**
@@ -140,6 +152,7 @@ export class MajikContact {
140
152
  fingerprint: serialized.fingerprint,
141
153
  meta: serialized.meta,
142
154
  publicKey: { raw: publicKeyRaw },
155
+ majikah_registered: serialized.majikah_registered,
143
156
  });
144
157
  }
145
158
  catch (err) {
@@ -162,6 +175,7 @@ export class MajikContact {
162
175
  updatedAt: identityJSON.timestamp,
163
176
  blocked: identityJSON.restricted,
164
177
  },
178
+ majikah_registered: true,
165
179
  };
166
180
  return new MajikContact(contactData);
167
181
  }
@@ -16,8 +16,9 @@ export declare class MajikMessageChat {
16
16
  private expires_at;
17
17
  private read_by;
18
18
  private conversation_id;
19
+ private permanent;
19
20
  private static readonly MAX_MESSAGE_LENGTH;
20
- constructor(id: MajikMessageChatID, account: MajikMessageAccountID, message: string, sender: MajikMessagePublicKey, recipients: MajikMessagePublicKey[], timestamp: string, expires_at: string, read_by?: string[], conversation_id?: string);
21
+ constructor(id: MajikMessageChatID, account: MajikMessageAccountID, message: string, sender: MajikMessagePublicKey, recipients: MajikMessagePublicKey[], timestamp: string, expires_at: string, read_by?: string[], conversation_id?: string, permanent?: boolean);
21
22
  getID(): string;
22
23
  getConversationID(): string;
23
24
  get account(): MajikMessageAccountID;
@@ -53,7 +54,7 @@ export declare class MajikMessageChat {
53
54
  * @returns Promise resolving to new MajikMessageChat instance with compressed message
54
55
  * @throws Error if validation or compression fails
55
56
  */
56
- static create(account: MajikMessageIdentity, message: string, recipients: string[], expiresInMs?: number): Promise<MajikMessageChat>;
57
+ static create(account: MajikMessageIdentity, message: string, recipients: string[], expiresInMs?: number, permanent?: boolean): Promise<MajikMessageChat>;
57
58
  /**
58
59
  * Generate a deterministic conversation ID from a message JSON
59
60
  * Reads sender and recipients directly from JSON without parsing
@@ -16,9 +16,10 @@ export class MajikMessageChat {
16
16
  expires_at;
17
17
  read_by;
18
18
  conversation_id;
19
+ permanent;
19
20
  // Maximum allowed length for the compressed message string
20
21
  static MAX_MESSAGE_LENGTH = 10000;
21
- constructor(id, account, message, sender, recipients, timestamp, expires_at, read_by = [], conversation_id) {
22
+ constructor(id, account, message, sender, recipients, timestamp, expires_at, read_by = [], conversation_id, permanent = false) {
22
23
  this.validateID(id);
23
24
  this.validateAccount(account);
24
25
  this.validateMessage(message);
@@ -35,6 +36,7 @@ export class MajikMessageChat {
35
36
  this.timestamp = timestamp;
36
37
  this.expires_at = expires_at;
37
38
  this.read_by = [...read_by]; // Clone to prevent external mutation
39
+ this.permanent = permanent;
38
40
  this.conversation_id = conversation_id || this.generateConversationID();
39
41
  }
40
42
  // ============= GETTERS =============
@@ -117,7 +119,7 @@ export class MajikMessageChat {
117
119
  * @returns Promise resolving to new MajikMessageChat instance with compressed message
118
120
  * @throws Error if validation or compression fails
119
121
  */
120
- static async create(account, message, recipients, expiresInMs = 24 * 60 * 60 * 1000) {
122
+ static async create(account, message, recipients, expiresInMs = 24 * 60 * 60 * 1000, permanent = false) {
121
123
  if (!account) {
122
124
  throw new Error("Invalid sender account: must be provided");
123
125
  }
@@ -153,7 +155,7 @@ export class MajikMessageChat {
153
155
  catch (error) {
154
156
  throw new Error(`Failed to compress message: ${error instanceof Error ? error.message : "Unknown error"}`);
155
157
  }
156
- return new MajikMessageChat(autogenerateID(), accountID, compressedMessage, senderID.trim(), recipients.map((r) => r.trim()).filter((r) => r !== ""), now.toISOString(), expiresAt.toISOString(), []);
158
+ return new MajikMessageChat(autogenerateID(), accountID, compressedMessage, senderID.trim(), recipients.map((r) => r.trim()).filter((r) => r !== ""), now.toISOString(), expiresAt.toISOString(), [], undefined, permanent);
157
159
  }
158
160
  // ============= STATIC HELPER METHODS =============
159
161
  /**
@@ -210,6 +212,9 @@ export class MajikMessageChat {
210
212
  }
211
213
  // ============= EXPIRATION METHODS =============
212
214
  isExpired() {
215
+ if (this.permanent) {
216
+ return false;
217
+ }
213
218
  const now = new Date();
214
219
  const expiresAt = new Date(this.expires_at);
215
220
  return now >= expiresAt;
@@ -292,7 +297,9 @@ export class MajikMessageChat {
292
297
  }
293
298
  // ============= READER MANAGEMENT =============
294
299
  markAsRead(userPublicKey) {
295
- if (!userPublicKey || typeof userPublicKey !== "string" || userPublicKey.trim() === "") {
300
+ if (!userPublicKey ||
301
+ typeof userPublicKey !== "string" ||
302
+ userPublicKey.trim() === "") {
296
303
  throw new Error("Invalid userPublicKey: must be a non-empty string");
297
304
  }
298
305
  const trimmedId = userPublicKey.trim();
@@ -355,6 +362,7 @@ export class MajikMessageChat {
355
362
  timestamp: this.timestamp,
356
363
  expires_at: this.expires_at,
357
364
  read_by: [...this.read_by],
365
+ permanent: this.permanent,
358
366
  };
359
367
  }
360
368
  static fromJSON(json) {
@@ -362,7 +370,7 @@ export class MajikMessageChat {
362
370
  if (!this.isValidJSON(rawParse)) {
363
371
  throw new Error("Invalid JSON: missing required fields or invalid types");
364
372
  }
365
- return new MajikMessageChat(rawParse.id, rawParse.account, rawParse.message, rawParse.sender, rawParse.recipients || [], rawParse.timestamp, rawParse.expires_at, rawParse.read_by || [], rawParse?.conversation_id);
373
+ return new MajikMessageChat(rawParse.id, rawParse.account, rawParse.message, rawParse.sender, rawParse.recipients || [], rawParse.timestamp, rawParse.expires_at, rawParse.read_by || [], rawParse?.conversation_id, rawParse?.permanent);
366
374
  }
367
375
  // ============= REDIS METHODS =============
368
376
  getRedisKey() {
@@ -9,5 +9,6 @@ export interface MajikMessageChatJSON {
9
9
  timestamp: string;
10
10
  expires_at: string;
11
11
  read_by: string[];
12
+ permanent: boolean;
12
13
  }
13
14
  export type RedisKey = string;
@@ -80,7 +80,7 @@ export declare class MajikMessage {
80
80
  backup: string;
81
81
  }>;
82
82
  addOwnAccount(account: MajikContact): void;
83
- listOwnAccounts(): MajikContact[];
83
+ listOwnAccounts(majikahOnly?: boolean): MajikContact[];
84
84
  getOwnAccountById(id: string): MajikContact | undefined;
85
85
  /**
86
86
  * Set an active account (moves it to index 0)
@@ -136,7 +136,10 @@ export declare class MajikMessage {
136
136
  updateContactMeta(id: string, meta: Partial<MajikContactMeta>): void;
137
137
  blockContact(id: string): void;
138
138
  unblockContact(id: string): void;
139
- listContacts(all?: boolean): MajikContact[];
139
+ listContacts(all?: boolean, majikahOnly?: boolean): MajikContact[];
140
+ isContactMajikahRegistered(id: string): boolean;
141
+ isContactMajikahIdentityChecked(id: string): boolean;
142
+ setContactMajikahStatus(id: string, status: boolean): void;
140
143
  /**
141
144
  * Update the passphrase for an identity.
142
145
  * - `id` defaults to the current active account if not provided.
@@ -148,10 +148,13 @@ export class MajikMessage {
148
148
  }
149
149
  this.scheduleAutosave();
150
150
  }
151
- listOwnAccounts() {
152
- const userAccounts = this.ownAccountsOrder
151
+ listOwnAccounts(majikahOnly = false) {
152
+ let userAccounts = this.ownAccountsOrder
153
153
  .map((id) => this.ownAccounts.get(id))
154
154
  .filter((c) => !!c);
155
+ if (majikahOnly) {
156
+ userAccounts = userAccounts.filter((acct) => this.isContactMajikahRegistered(acct.id));
157
+ }
155
158
  return userAccounts;
156
159
  }
157
160
  getOwnAccountById(id) {
@@ -421,15 +424,25 @@ export class MajikMessage {
421
424
  this.contactDirectory.unblockContact(id);
422
425
  this.scheduleAutosave();
423
426
  }
424
- listContacts(all = true) {
425
- const contacts = this.contactDirectory.listContacts(true);
427
+ listContacts(all = true, majikahOnly = false) {
428
+ const contacts = this.contactDirectory.listContacts(true, majikahOnly);
426
429
  if (all) {
427
430
  return contacts;
428
431
  }
429
- const userAccounts = this.listOwnAccounts();
432
+ const userAccounts = this.listOwnAccounts(majikahOnly);
430
433
  const userAccountIds = new Set(userAccounts.map((a) => a.id));
431
434
  return contacts.filter((contact) => !userAccountIds.has(contact.id));
432
435
  }
436
+ isContactMajikahRegistered(id) {
437
+ return this.contactDirectory.isMajikahRegistered(id);
438
+ }
439
+ isContactMajikahIdentityChecked(id) {
440
+ return this.contactDirectory.isMajikahIdentityChecked(id);
441
+ }
442
+ setContactMajikahStatus(id, status) {
443
+ this.contactDirectory.setMajikahStatus(id, status);
444
+ this.scheduleAutosave();
445
+ }
433
446
  /**
434
447
  * Update the passphrase for an identity.
435
448
  * - `id` defaults to the current active account if not provided.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@thezelijah/majik-message",
3
3
  "type": "module",
4
4
  "description": "Encrypt and decrypt messages on any website. Secure chats with keypairs and seed-based accounts. Open source.",
5
- "version": "1.0.16",
5
+ "version": "1.0.17",
6
6
  "license": "Apache-2.0",
7
7
  "author": "Zelijah",
8
8
  "main": "./dist/index.js",