@majikah/majik-message 0.3.7 → 0.3.9

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.
@@ -2,9 +2,11 @@ import { MajikContact, MajikContactData, MajikContactGroup, MajikContactGroupMet
2
2
  import { MajikContactDirectory } from "./majik-contact-directory";
3
3
  import { MajikContactGroupManager } from "./majik-contact-groups";
4
4
  import { MAJIK_API_RESPONSE } from "../types";
5
- import { MajikContactManagerJSON } from "./types";
5
+ import { ContactManagerQueryMode, MajikContactManagerJSON } from "./types";
6
6
  import { MajikContactStorageAdapter } from "../storage/contact-directory/contacts/_types";
7
7
  import { MajikContactGroupStorageAdapter } from "../storage/contact-directory/groups/_types";
8
+ import { MajikRecipient } from "@majikah/majik-envelope";
9
+ import { ExpectedSigner } from "@majikah/majik-signature";
8
10
  export interface MajikContactManagerAdapters {
9
11
  contacts?: MajikContactStorageAdapter;
10
12
  groups?: MajikContactGroupStorageAdapter;
@@ -83,6 +85,10 @@ export declare class MajikContactManager {
83
85
  getContact(id: string): MajikContact | undefined;
84
86
  getContactByFingerprint(fingerprint: string): MajikContact | undefined;
85
87
  getContactByPublicKeyBase64(publicKeyBase64: string): Promise<MajikContact | undefined>;
88
+ getContactsByIds(ids: string[], strict?: boolean): MajikContact[];
89
+ getContactsByPublicKeys(publicKeys: string[], strict?: boolean): Promise<MajikContact[]>;
90
+ getMajikRecipients(mode: ContactManagerQueryMode | undefined, input: string[], strict?: boolean): Promise<MajikRecipient[]>;
91
+ getExpectedSigners(mode: ContactManagerQueryMode | undefined, input: string[], strict?: boolean): Promise<ExpectedSigner[]>;
86
92
  hasContact(id: string): boolean;
87
93
  hasFingerprint(fingerprint: string): boolean;
88
94
  hasContactByPublicKeyBase64(publicKeyBase64: string): Promise<boolean>;
@@ -2,11 +2,12 @@ import { MajikContact, } from "@majikah/majik-contact";
2
2
  import { MajikContactDirectory } from "./majik-contact-directory";
3
3
  import { MajikContactGroupManager } from "./majik-contact-groups";
4
4
  import { MajikContactManagerError } from "./errors";
5
- import { arrayBufferToBase64, arrayToBase64, base64ToArrayBuffer, } from "../utils/utilities";
5
+ import { arrayBufferToBase64, arrayToBase64, base64ToArrayBuffer, base64ToUint8Array, } from "../utils/utilities";
6
6
  import { KEY_ALGO } from "../crypto/constants";
7
7
  import { gunzipSync, gzipSync } from "fflate";
8
8
  import { InMemoryContactAdapter } from "../storage/contact-directory/contacts/adapter-memory";
9
9
  import { InMemoryContactGroupAdapter } from "../storage/contact-directory/groups/adapter-memory";
10
+ import { MajikEnvelope } from "@majikah/majik-envelope";
10
11
  export class MajikContactManager {
11
12
  directory;
12
13
  groupManager;
@@ -191,7 +192,106 @@ export class MajikContactManager {
191
192
  return this.directory.getContactByFingerprint(fingerprint);
192
193
  }
193
194
  async getContactByPublicKeyBase64(publicKeyBase64) {
194
- return this.directory.getContactByPublicKeyBase64(publicKeyBase64);
195
+ return await this.directory.getContactByPublicKeyBase64(publicKeyBase64);
196
+ }
197
+ getContactsByIds(ids, strict = false) {
198
+ if (!ids?.length)
199
+ return [];
200
+ const seen = new Set();
201
+ const results = [];
202
+ for (const id of ids) {
203
+ if (seen.has(id))
204
+ continue;
205
+ seen.add(id);
206
+ const contact = this.directory.getContact(id);
207
+ if (!contact) {
208
+ if (strict) {
209
+ throw new MajikContactManagerError(`Contact not found: ${id}`);
210
+ }
211
+ continue;
212
+ }
213
+ results.push(contact);
214
+ }
215
+ return results;
216
+ }
217
+ async getContactsByPublicKeys(publicKeys, strict = false) {
218
+ if (!publicKeys?.length)
219
+ return [];
220
+ const uniqueKeys = [...new Set(publicKeys)];
221
+ const contacts = await Promise.all(uniqueKeys.map(async (key) => {
222
+ const contact = await this.directory.getContactByPublicKeyBase64(key);
223
+ if (!contact && strict) {
224
+ throw new MajikContactManagerError(`Contact not found for publicKey: ${key}`);
225
+ }
226
+ return contact;
227
+ }));
228
+ return contacts.filter((c) => Boolean(c));
229
+ }
230
+ async getMajikRecipients(mode = "id", input, strict) {
231
+ if (!input?.length)
232
+ throw new MajikContactManagerError("At least 1 id/key is required");
233
+ const contacts = mode === "public_key"
234
+ ? await this.getContactsByPublicKeys(input, strict)
235
+ : this.getContactsByIds(input, strict);
236
+ if (!contacts || contacts.length === 0)
237
+ return [];
238
+ const recipients = [];
239
+ const seen = new Set();
240
+ const invalidContacts = [];
241
+ for (const contact of contacts) {
242
+ if (!contact)
243
+ continue;
244
+ // dedupe by fingerprint
245
+ if (seen.has(contact.fingerprint))
246
+ continue;
247
+ const mlPubKey = base64ToUint8Array(contact.mlKey);
248
+ if (!mlPubKey) {
249
+ invalidContacts.push(contact.fingerprint);
250
+ continue;
251
+ }
252
+ const builtMajikRecipient = await MajikEnvelope.buildMajikRecipientFromContact(contact);
253
+ recipients.push(builtMajikRecipient);
254
+ seen.add(contact.fingerprint);
255
+ }
256
+ if (invalidContacts.length > 0) {
257
+ throw new MajikContactManagerError(`Invalid ML-KEM public key for contact(s): ${invalidContacts.join(", ")}`);
258
+ }
259
+ return recipients;
260
+ }
261
+ async getExpectedSigners(mode = "id", input, strict) {
262
+ if (!input?.length)
263
+ throw new MajikContactManagerError("At least 1 id/key is required");
264
+ const contacts = mode === "public_key"
265
+ ? await this.getContactsByPublicKeys(input, strict)
266
+ : this.getContactsByIds(input, strict);
267
+ if (!contacts || contacts.length === 0)
268
+ return [];
269
+ const signers = [];
270
+ const seen = new Set();
271
+ const invalidContacts = [];
272
+ for (const contact of contacts) {
273
+ if (!contact)
274
+ continue;
275
+ // dedupe by fingerprint
276
+ if (seen.has(contact.fingerprint))
277
+ continue;
278
+ const mlDsaPublicKey = contact.mlDsaPublicKeyBase64;
279
+ if (!mlDsaPublicKey?.trim()) {
280
+ invalidContacts.push(contact.fingerprint);
281
+ continue;
282
+ }
283
+ const expectedSigner = {
284
+ edPublicKey: contact.edPublicKeyBase64,
285
+ mlDsaPublicKey: contact.mlDsaPublicKeyBase64,
286
+ signerId: contact.fingerprint,
287
+ };
288
+ signers.push(expectedSigner);
289
+ seen.add(contact.fingerprint);
290
+ }
291
+ if (invalidContacts.length > 0) {
292
+ throw new MajikContactManagerError(`Invalid ML-KEM public key for contact(s): ${invalidContacts.join(", ")}`);
293
+ }
294
+ return signers;
195
295
  }
196
296
  hasContact(id) {
197
297
  return this.directory.hasContact(id);
@@ -1,4 +1,5 @@
1
1
  import { SerializedMajikContact, SerializedMajikContactGroup } from "@majikah/majik-contact";
2
+ export type ContactManagerQueryMode = "id" | "public_key";
2
3
  export interface MajikContactManagerJSON {
3
4
  contacts: MajikContactDirectoryData;
4
5
  groups: MajikContactGroupManagerData;
@@ -2,7 +2,7 @@ import { MajikMessageChatJSON } from "../../database/chat/types";
2
2
  import { SQLiteDatabase } from "../sql-db-manager";
3
3
  import { StorageSource } from "../storage-adapter";
4
4
  import { MajikMessageChatStorageAdapter } from "./_types";
5
- export declare class SQLiteInvoiceAdapter implements MajikMessageChatStorageAdapter {
5
+ export declare class SQLiteMessageChatsAdapter implements MajikMessageChatStorageAdapter {
6
6
  private db;
7
7
  constructor(db: SQLiteDatabase);
8
8
  save(message: MajikMessageChatJSON, source?: StorageSource): Promise<void>;
@@ -1,24 +1,25 @@
1
- export class SQLiteInvoiceAdapter {
1
+ import { MAJIKAH_SQL_TABLES } from "../sql-schema";
2
+ export class SQLiteMessageChatsAdapter {
2
3
  db;
3
4
  constructor(db) {
4
5
  this.db = db;
5
6
  }
6
- async save(message, source = "local") {
7
- const resolvedSource = source ?? "local";
8
- await this.db.run(`INSERT OR REPLACE INTO majik_message_chats
7
+ async save(message, source = `local`) {
8
+ const resolvedSource = source ?? `local`;
9
+ await this.db.run(`INSERT OR REPLACE INTO ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_CHATS}
9
10
  (id, json, created_at, source)
10
11
  VALUES (?, ?, ?, ?)`, [message.id, JSON.stringify(message), message.timestamp, resolvedSource]);
11
12
  }
12
13
  async getById(id, source) {
13
14
  const row = source
14
- ? await this.db.get("SELECT json FROM majik_messages WHERE id = ? AND source = ?", [id, source])
15
- : await this.db.get("SELECT json FROM majik_messages WHERE id = ?", [id]);
15
+ ? await this.db.get(`SELECT json FROM ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_CHATS} WHERE id = ? AND source = ?`, [id, source])
16
+ : await this.db.get(`SELECT json FROM ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_CHATS} WHERE id = ?`, [id]);
16
17
  return row ? JSON.parse(row.json) : null;
17
18
  }
18
19
  async list(source) {
19
20
  const rows = source
20
- ? await this.db.all("SELECT json FROM majik_messages WHERE source = ?", [source])
21
- : await this.db.all("SELECT json FROM majik_messages");
21
+ ? await this.db.all(`SELECT json FROM ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_CHATS} WHERE source = ?`, [source])
22
+ : await this.db.all(`SELECT json FROM ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_CHATS}`);
22
23
  return rows.map((r) => JSON.parse(r.json));
23
24
  }
24
25
  async remove(id, source) {
@@ -26,42 +27,42 @@ export class SQLiteInvoiceAdapter {
26
27
  if (!exists)
27
28
  return false;
28
29
  if (source) {
29
- await this.db.run("DELETE FROM majik_messages WHERE id = ? AND source = ?", [id, source]);
30
+ await this.db.run(`DELETE FROM ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_CHATS} WHERE id = ? AND source = ?`, [id, source]);
30
31
  }
31
32
  else {
32
- await this.db.run("DELETE FROM majik_messages WHERE id = ?", [id]);
33
+ await this.db.run(`DELETE FROM ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_CHATS} WHERE id = ?`, [id]);
33
34
  }
34
35
  return true;
35
36
  }
36
37
  async clear(source) {
37
38
  if (source) {
38
- await this.db.run("DELETE FROM majik_messages WHERE source = ?", [
39
+ await this.db.run(`DELETE FROM ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_CHATS} WHERE source = ?`, [
39
40
  source,
40
41
  ]);
41
42
  }
42
43
  else {
43
- await this.db.run("DELETE FROM majik_messages");
44
+ await this.db.run(`DELETE FROM ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_CHATS}`);
44
45
  }
45
46
  }
46
47
  async count(source) {
47
48
  const row = source
48
- ? await this.db.get("SELECT COUNT(*) as n FROM majik_messages WHERE source = ?", [source])
49
- : await this.db.get("SELECT COUNT(*) as n FROM majik_messages");
49
+ ? await this.db.get(`SELECT COUNT(*) as n FROM ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_CHATS} WHERE source = ?`, [source])
50
+ : await this.db.get(`SELECT COUNT(*) as n FROM ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_CHATS}`);
50
51
  return row?.n ?? 0;
51
52
  }
52
53
  async exists(id, source) {
53
54
  const row = source
54
- ? await this.db.get("SELECT 1 FROM majik_messages WHERE id = ? AND source = ?", [id, source])
55
- : await this.db.get("SELECT 1 FROM majik_messages WHERE id = ?", [id]);
55
+ ? await this.db.get(`SELECT 1 FROM ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_CHATS} WHERE id = ? AND source = ?`, [id, source])
56
+ : await this.db.get(`SELECT 1 FROM ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_CHATS} WHERE id = ?`, [id]);
56
57
  return !!row;
57
58
  }
58
- async bulkSave(messages, source = "local") {
59
+ async bulkSave(messages, source = `local`) {
59
60
  if (messages.length === 0)
60
61
  return;
61
- const resolvedSource = source ?? "local";
62
+ const resolvedSource = source ?? `local`;
62
63
  await this.db.transaction(async (tx) => {
63
64
  for (const msg of messages) {
64
- await tx.run(`INSERT OR REPLACE INTO majik_message_chats
65
+ await tx.run(`INSERT OR REPLACE INTO ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_CHATS}
65
66
  (id, json, created_at, source)
66
67
  VALUES (?, ?, ?, ?, ?, ?)`, [msg.id, JSON.stringify(msg), msg.timestamp, resolvedSource]);
67
68
  }
@@ -73,10 +74,10 @@ export class SQLiteInvoiceAdapter {
73
74
  await this.db.transaction(async (tx) => {
74
75
  for (const id of ids) {
75
76
  if (source) {
76
- await tx.run("DELETE FROM majik_messages WHERE id = ? AND source = ?", [id, source]);
77
+ await tx.run(`DELETE FROM ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_CHATS} WHERE id = ? AND source = ?`, [id, source]);
77
78
  }
78
79
  else {
79
- await tx.run("DELETE FROM majik_messages WHERE id = ?", [id]);
80
+ await tx.run(`DELETE FROM ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_CHATS} WHERE id = ?`, [id]);
80
81
  }
81
82
  }
82
83
  });
@@ -1,6 +1,7 @@
1
1
  import { SerializedMajikContact } from "@majikah/majik-contact";
2
2
  import { MajikContactStorageAdapter } from "./_types";
3
3
  import { SQLiteDatabase } from "../../sql-db-manager";
4
+ import { StorageQuery } from "../../storage-adapter";
4
5
  export declare class SQLiteContactAdapter implements MajikContactStorageAdapter {
5
6
  private db;
6
7
  constructor(db: SQLiteDatabase);
@@ -13,4 +14,5 @@ export declare class SQLiteContactAdapter implements MajikContactStorageAdapter
13
14
  exists(id: string): Promise<boolean>;
14
15
  bulkSave(contacts: SerializedMajikContact[]): Promise<void>;
15
16
  bulkRemove(ids: string[]): Promise<void>;
17
+ query(query: StorageQuery<SerializedMajikContact>): Promise<SerializedMajikContact[]>;
16
18
  }
@@ -1,10 +1,11 @@
1
+ import { MAJIKAH_SQL_TABLES } from "../../sql-schema";
1
2
  export class SQLiteContactAdapter {
2
3
  db;
3
4
  constructor(db) {
4
5
  this.db = db;
5
6
  }
6
7
  async save(contact) {
7
- await this.db.run(`INSERT OR REPLACE INTO majik_contacts
8
+ await this.db.run(`INSERT OR REPLACE INTO ${MAJIKAH_SQL_TABLES.MAJIK_CONTACTS}
8
9
  (id, json, fingerprint, label, created_at, updated_at)
9
10
  VALUES (?, ?, ?, ?, ?, ?)`, [
10
11
  contact.id,
@@ -16,31 +17,29 @@ export class SQLiteContactAdapter {
16
17
  ]);
17
18
  }
18
19
  async getById(id) {
19
- const row = await this.db.get("SELECT json FROM majik_contacts WHERE id = ?", [id]);
20
+ const row = await this.db.get(`SELECT json FROM ${MAJIKAH_SQL_TABLES.MAJIK_CONTACTS} WHERE id = ?`, [id]);
20
21
  return row ? JSON.parse(row.json) : null;
21
22
  }
22
23
  async list() {
23
- const rows = await this.db.all("SELECT json FROM majik_contacts");
24
+ const rows = await this.db.all(`SELECT json FROM ${MAJIKAH_SQL_TABLES.MAJIK_CONTACTS}`);
24
25
  return rows.map((r) => JSON.parse(r.json));
25
26
  }
26
27
  async remove(id) {
27
28
  const exists = await this.exists(id);
28
29
  if (!exists)
29
30
  return false;
30
- await this.db.run("DELETE FROM majik_contacts WHERE id = ?", [id]);
31
+ await this.db.run(`DELETE FROM ${MAJIKAH_SQL_TABLES.MAJIK_CONTACTS} WHERE id = ?`, [id]);
31
32
  return true;
32
33
  }
33
34
  async clear() {
34
- await this.db.run("DELETE FROM majik_contacts");
35
+ await this.db.run(`DELETE FROM ${MAJIKAH_SQL_TABLES.MAJIK_CONTACTS}`);
35
36
  }
36
37
  async count() {
37
- const row = await this.db.get("SELECT COUNT(*) as n FROM majik_contacts");
38
+ const row = await this.db.get(`SELECT COUNT(*) as n FROM ${MAJIKAH_SQL_TABLES.MAJIK_CONTACTS}`);
38
39
  return row?.n ?? 0;
39
40
  }
40
41
  async exists(id) {
41
- const row = await this.db.get("SELECT 1 FROM majik_contacts WHERE id = ?", [
42
- id,
43
- ]);
42
+ const row = await this.db.get(`SELECT 1 FROM ${MAJIKAH_SQL_TABLES.MAJIK_CONTACTS} WHERE id = ?`, [id]);
44
43
  return !!row;
45
44
  }
46
45
  async bulkSave(contacts) {
@@ -48,7 +47,7 @@ export class SQLiteContactAdapter {
48
47
  return;
49
48
  await this.db.transaction(async (tx) => {
50
49
  for (const c of contacts) {
51
- await tx.run(`INSERT OR REPLACE INTO majik_contacts
50
+ await tx.run(`INSERT OR REPLACE INTO ${MAJIKAH_SQL_TABLES.MAJIK_CONTACTS}
52
51
  (id, json, fingerprint, label, created_at, updated_at)
53
52
  VALUES (?, ?, ?, ?, ?, ?)`, [
54
53
  c.id,
@@ -66,8 +65,33 @@ export class SQLiteContactAdapter {
66
65
  return;
67
66
  await this.db.transaction(async (tx) => {
68
67
  for (const id of ids) {
69
- await tx.run("DELETE FROM majik_contacts WHERE id = ?", [id]);
68
+ await tx.run(`DELETE FROM ${MAJIKAH_SQL_TABLES.MAJIK_CONTACTS} WHERE id = ?`, [id]);
70
69
  }
71
70
  });
72
71
  }
72
+ async query(query) {
73
+ const clauses = [];
74
+ const values = [];
75
+ if (query.where) {
76
+ for (const [key, value] of Object.entries(query.where)) {
77
+ clauses.push(`${key} = ?`);
78
+ values.push(value);
79
+ }
80
+ }
81
+ let sql = `SELECT json FROM ${MAJIKAH_SQL_TABLES.MAJIK_CONTACTS}`;
82
+ if (clauses.length > 0) {
83
+ sql += ` WHERE ${clauses.join(" AND ")}`;
84
+ }
85
+ if (query.orderBy) {
86
+ sql += ` ORDER BY ${String(query.orderBy)} ${query.orderDirection ?? "asc"}`;
87
+ }
88
+ if (query.limit) {
89
+ sql += ` LIMIT ${query.limit}`;
90
+ }
91
+ if (query.offset) {
92
+ sql += ` OFFSET ${query.offset}`;
93
+ }
94
+ const rows = await this.db.all(sql, values);
95
+ return rows.map((r) => JSON.parse(r.json));
96
+ }
73
97
  }
@@ -1,10 +1,11 @@
1
+ import { MAJIKAH_SQL_TABLES } from "../../sql-schema";
1
2
  export class SQLiteContactGroupAdapter {
2
3
  db;
3
4
  constructor(db) {
4
5
  this.db = db;
5
6
  }
6
7
  async save(group) {
7
- await this.db.run(`INSERT OR REPLACE INTO majik_contact_groups
8
+ await this.db.run(`INSERT OR REPLACE INTO ${MAJIKAH_SQL_TABLES.MAJIK_CONTACT_GROUPS}
8
9
  (id, json, name, created_at, updated_at, is_system)
9
10
  VALUES (?, ?, ?, ?, ?, ?)`, [
10
11
  group.id,
@@ -16,29 +17,29 @@ export class SQLiteContactGroupAdapter {
16
17
  ]);
17
18
  }
18
19
  async getById(id) {
19
- const row = await this.db.get("SELECT json FROM majik_contact_groups WHERE id = ?", [id]);
20
+ const row = await this.db.get(`SELECT json FROM ${MAJIKAH_SQL_TABLES.MAJIK_CONTACT_GROUPS} WHERE id = ?`, [id]);
20
21
  return row ? JSON.parse(row.json) : null;
21
22
  }
22
23
  async list() {
23
- const rows = await this.db.all("SELECT json FROM majik_contact_groups");
24
+ const rows = await this.db.all(`SELECT json FROM ${MAJIKAH_SQL_TABLES.MAJIK_CONTACT_GROUPS}`);
24
25
  return rows.map((r) => JSON.parse(r.json));
25
26
  }
26
27
  async remove(id) {
27
28
  const exists = await this.exists(id);
28
29
  if (!exists)
29
30
  return false;
30
- await this.db.run("DELETE FROM majik_contact_groups WHERE id = ?", [id]);
31
+ await this.db.run(`DELETE FROM ${MAJIKAH_SQL_TABLES.MAJIK_CONTACT_GROUPS} WHERE id = ?`, [id]);
31
32
  return true;
32
33
  }
33
34
  async clear() {
34
- await this.db.run("DELETE FROM majik_contact_groups");
35
+ await this.db.run(`DELETE FROM ${MAJIKAH_SQL_TABLES.MAJIK_CONTACT_GROUPS}`);
35
36
  }
36
37
  async count() {
37
- const row = await this.db.get("SELECT COUNT(*) as n FROM majik_contact_groups");
38
+ const row = await this.db.get(`SELECT COUNT(*) as n FROM ${MAJIKAH_SQL_TABLES.MAJIK_CONTACT_GROUPS}`);
38
39
  return row?.n ?? 0;
39
40
  }
40
41
  async exists(id) {
41
- const row = await this.db.get("SELECT 1 FROM majik_contact_groups WHERE id = ?", [id]);
42
+ const row = await this.db.get(`SELECT 1 FROM ${MAJIKAH_SQL_TABLES.MAJIK_CONTACT_GROUPS} WHERE id = ?`, [id]);
42
43
  return !!row;
43
44
  }
44
45
  async bulkSave(groups) {
@@ -46,7 +47,7 @@ export class SQLiteContactGroupAdapter {
46
47
  return;
47
48
  await this.db.transaction(async (tx) => {
48
49
  for (const g of groups) {
49
- await tx.run(`INSERT OR REPLACE INTO majik_contact_groups
50
+ await tx.run(`INSERT OR REPLACE INTO ${MAJIKAH_SQL_TABLES.MAJIK_CONTACT_GROUPS}
50
51
  (id, json, name, created_at, updated_at, is_system)
51
52
  VALUES (?, ?, ?, ?, ?, ?)`, [
52
53
  g.id,
@@ -64,7 +65,7 @@ export class SQLiteContactGroupAdapter {
64
65
  return;
65
66
  await this.db.transaction(async (tx) => {
66
67
  for (const id of ids) {
67
- await tx.run("DELETE FROM majik_contact_groups WHERE id = ?", [id]);
68
+ await tx.run(`DELETE FROM ${MAJIKAH_SQL_TABLES.MAJIK_CONTACT_GROUPS} WHERE id = ?`, [id]);
68
69
  }
69
70
  });
70
71
  }
@@ -1,6 +1,7 @@
1
1
  import { MajikKeyJSON } from "@majikah/majik-key";
2
2
  import { MajikKeyStorageAdapter } from "./_types";
3
3
  import { SQLiteDatabase } from "../sql-db-manager";
4
+ import { StorageQuery } from "../storage-adapter";
4
5
  export declare class SQLiteKeystoreAdapter implements MajikKeyStorageAdapter {
5
6
  private db;
6
7
  constructor(db: SQLiteDatabase);
@@ -13,4 +14,5 @@ export declare class SQLiteKeystoreAdapter implements MajikKeyStorageAdapter {
13
14
  exists(id: string): Promise<boolean>;
14
15
  bulkSave(keys: MajikKeyJSON[]): Promise<void>;
15
16
  bulkRemove(ids: string[]): Promise<void>;
17
+ query(query: StorageQuery<MajikKeyJSON>): Promise<MajikKeyJSON[]>;
16
18
  }
@@ -1,10 +1,11 @@
1
+ import { MAJIKAH_SQL_TABLES } from "../sql-schema";
1
2
  export class SQLiteKeystoreAdapter {
2
3
  db;
3
4
  constructor(db) {
4
5
  this.db = db;
5
6
  }
6
7
  async save(key) {
7
- await this.db.run(`INSERT OR REPLACE INTO majik_keys
8
+ await this.db.run(`INSERT OR REPLACE INTO ${MAJIKAH_SQL_TABLES.MAJIK_KEYS}
8
9
  (id, json, timestamp, public_key)
9
10
  VALUES (?, ?, ?, ?)`, [
10
11
  key.id,
@@ -14,31 +15,29 @@ export class SQLiteKeystoreAdapter {
14
15
  ]);
15
16
  }
16
17
  async getById(id) {
17
- const row = await this.db.get("SELECT json FROM majik_keys WHERE id = ?", [id]);
18
+ const row = await this.db.get("SELECT json FROM ${MAJIKAH_SQL_TABLES.MAJIK_KEYS} WHERE id = ?", [id]);
18
19
  return row ? JSON.parse(row.json) : null;
19
20
  }
20
21
  async list() {
21
- const rows = await this.db.all("SELECT json FROM majik_keys");
22
+ const rows = await this.db.all(`SELECT json FROM ${MAJIKAH_SQL_TABLES.MAJIK_KEYS}`);
22
23
  return rows.map((r) => JSON.parse(r.json));
23
24
  }
24
25
  async remove(id) {
25
26
  const exists = await this.exists(id);
26
27
  if (!exists)
27
28
  return false;
28
- await this.db.run("DELETE FROM majik_keys WHERE id = ?", [id]);
29
+ await this.db.run(`DELETE FROM ${MAJIKAH_SQL_TABLES.MAJIK_KEYS} WHERE id = ?`, [id]);
29
30
  return true;
30
31
  }
31
32
  async clear() {
32
- await this.db.run("DELETE FROM majik_keys");
33
+ await this.db.run(`DELETE FROM ${MAJIKAH_SQL_TABLES.MAJIK_KEYS}`);
33
34
  }
34
35
  async count() {
35
- const row = await this.db.get("SELECT COUNT(*) as n FROM majik_keys");
36
+ const row = await this.db.get(`SELECT COUNT(*) as n FROM ${MAJIKAH_SQL_TABLES.MAJIK_KEYS}`);
36
37
  return row?.n ?? 0;
37
38
  }
38
39
  async exists(id) {
39
- const row = await this.db.get("SELECT 1 FROM majik_keys WHERE id = ?", [
40
- id,
41
- ]);
40
+ const row = await this.db.get(`SELECT 1 FROM ${MAJIKAH_SQL_TABLES.MAJIK_KEYS} WHERE id = ?`, [id]);
42
41
  return !!row;
43
42
  }
44
43
  async bulkSave(keys) {
@@ -46,7 +45,7 @@ export class SQLiteKeystoreAdapter {
46
45
  return;
47
46
  await this.db.transaction(async (tx) => {
48
47
  for (const g of keys) {
49
- await tx.run(`INSERT OR REPLACE INTO majik_keys
48
+ await tx.run(`INSERT OR REPLACE INTO ${MAJIKAH_SQL_TABLES.MAJIK_KEYS}
50
49
  (id, json, timestamp, public_key)
51
50
  VALUES (?, ?, ?, ?)`, [
52
51
  g.id,
@@ -62,8 +61,33 @@ export class SQLiteKeystoreAdapter {
62
61
  return;
63
62
  await this.db.transaction(async (tx) => {
64
63
  for (const id of ids) {
65
- await tx.run("DELETE FROM majik_keys WHERE id = ?", [id]);
64
+ await tx.run(`DELETE FROM ${MAJIKAH_SQL_TABLES.MAJIK_KEYS} WHERE id = ?`, [id]);
66
65
  }
67
66
  });
68
67
  }
68
+ async query(query) {
69
+ const clauses = [];
70
+ const values = [];
71
+ if (query.where) {
72
+ for (const [key, value] of Object.entries(query.where)) {
73
+ clauses.push(`${key} = ?`);
74
+ values.push(value);
75
+ }
76
+ }
77
+ let sql = `SELECT json FROM ${MAJIKAH_SQL_TABLES.MAJIK_KEYS}`;
78
+ if (clauses.length > 0) {
79
+ sql += ` WHERE ${clauses.join(" AND ")}`;
80
+ }
81
+ if (query.orderBy) {
82
+ sql += ` ORDER BY ${String(query.orderBy)} ${query.orderDirection ?? "asc"}`;
83
+ }
84
+ if (query.limit) {
85
+ sql += ` LIMIT ${query.limit}`;
86
+ }
87
+ if (query.offset) {
88
+ sql += ` OFFSET ${query.offset}`;
89
+ }
90
+ const rows = await this.db.all(sql, values);
91
+ return rows.map((r) => JSON.parse(r.json));
92
+ }
69
93
  }
@@ -1,4 +1,19 @@
1
1
  type MajikahSQLSchema = string;
2
+ /**
3
+ * Centralized SQLite table registry.
4
+ * - `as const` keeps literal types
5
+ * - `MajikahSQLTable` becomes a strict union type
6
+ */
7
+ export declare const MAJIKAH_SQL_TABLES: {
8
+ readonly MAJIK_CLIENT_STATE: "majik_client_state";
9
+ readonly MAJIK_KEYS: "majik_keys";
10
+ readonly MAJIK_MESSAGE_CHATS: "majik_message_chats";
11
+ readonly MAJIK_MESSAGE_FILES: "majik_message_files";
12
+ readonly MAJIK_MESSAGE_THREAD_MAILS: "majik_message_thread_mails";
13
+ readonly MAJIK_CONTACTS: "majik_contacts";
14
+ readonly MAJIK_CONTACT_GROUPS: "majik_contact_groups";
15
+ };
16
+ export type MajikahSQLTable = (typeof MAJIKAH_SQL_TABLES)[keyof typeof MAJIKAH_SQL_TABLES];
2
17
  export declare function buildSchemaSQL(schemas: MajikahSQLSchema[]): MajikahSQLSchema;
3
18
  export declare const MAJIKAH_SQL_SCHEMA_MAJIK_CLIENT_STATE: MajikahSQLSchema;
4
19
  export declare const MAJIKAH_SQL_SCHEMA_MAJIK_KEYS: MajikahSQLSchema;
@@ -1,3 +1,17 @@
1
+ /**
2
+ * Centralized SQLite table registry.
3
+ * - `as const` keeps literal types
4
+ * - `MajikahSQLTable` becomes a strict union type
5
+ */
6
+ export const MAJIKAH_SQL_TABLES = {
7
+ MAJIK_CLIENT_STATE: "majik_client_state",
8
+ MAJIK_KEYS: "majik_keys",
9
+ MAJIK_MESSAGE_CHATS: "majik_message_chats",
10
+ MAJIK_MESSAGE_FILES: "majik_message_files",
11
+ MAJIK_MESSAGE_THREAD_MAILS: "majik_message_thread_mails",
12
+ MAJIK_CONTACTS: "majik_contacts",
13
+ MAJIK_CONTACT_GROUPS: "majik_contact_groups",
14
+ };
1
15
  function normalizeSQL(sql) {
2
16
  return sql
3
17
  .trim()
@@ -19,28 +33,28 @@ export function buildSchemaSQL(schemas) {
19
33
  .join("\n\n");
20
34
  }
21
35
  export const MAJIKAH_SQL_SCHEMA_MAJIK_CLIENT_STATE = `
22
- CREATE TABLE IF NOT EXISTS majik_client_state (
36
+ CREATE TABLE IF NOT EXISTS ${MAJIKAH_SQL_TABLES.MAJIK_CLIENT_STATE} (
23
37
  key TEXT PRIMARY KEY,
24
38
  value TEXT NOT NULL,
25
39
  updated_at TEXT DEFAULT (datetime('now'))
26
40
  );
27
41
  `;
28
42
  export const MAJIKAH_SQL_SCHEMA_MAJIK_KEYS = `
29
- CREATE TABLE IF NOT EXISTS majik_keys (
43
+ CREATE TABLE IF NOT EXISTS ${MAJIKAH_SQL_TABLES.MAJIK_KEYS} (
30
44
  id TEXT PRIMARY KEY,
31
45
  json TEXT NOT NULL,
32
46
  timestamp TEXT NOT NULL,
33
47
  public_key TEXT NOT NULL
34
48
  );
35
49
 
36
- CREATE INDEX IF NOT EXISTS idx_majik_keys_timestamp
37
- ON majik_keys(timestamp);
50
+ CREATE INDEX IF NOT EXISTS idx_majik_keys_timestamp
51
+ ON ${MAJIKAH_SQL_TABLES.MAJIK_KEYS}(timestamp);
38
52
 
39
- CREATE INDEX IF NOT EXISTS idx_majik_keys_public_key
40
- ON majik_keys(public_key);
53
+ CREATE INDEX IF NOT EXISTS idx_majik_keys_public_key
54
+ ON ${MAJIKAH_SQL_TABLES.MAJIK_KEYS}(public_key);
41
55
  `;
42
56
  export const MAJIKAH_SQL_SCHEMA_MAJIK_MESSAGE_CHATS = `
43
- CREATE TABLE IF NOT EXISTS majik_message_chats (
57
+ CREATE TABLE IF NOT EXISTS ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_CHATS} (
44
58
  id TEXT PRIMARY KEY,
45
59
  json TEXT NOT NULL,
46
60
  created_at TEXT NOT NULL,
@@ -49,14 +63,14 @@ CREATE TABLE IF NOT EXISTS majik_message_chats (
49
63
  );
50
64
 
51
65
  CREATE INDEX IF NOT EXISTS idx_majik_message_chats_created_at
52
- ON majik_message_chats(created_at);
66
+ ON ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_CHATS}(created_at);
53
67
 
54
68
 
55
69
  CREATE INDEX IF NOT EXISTS idx_majik_message_chats_source
56
- ON majik_message_chats(source);
70
+ ON ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_CHATS}(source);
57
71
  `;
58
72
  export const MAJIKAH_SQL_SCHEMA_MAJIK_MESSAGE_FILES = `
59
- CREATE TABLE IF NOT EXISTS majik_message_files (
73
+ CREATE TABLE IF NOT EXISTS ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_FILES} (
60
74
  id TEXT PRIMARY KEY,
61
75
  json TEXT NOT NULL,
62
76
  created_at TEXT NOT NULL,
@@ -66,14 +80,14 @@ CREATE TABLE IF NOT EXISTS majik_message_files (
66
80
  );
67
81
 
68
82
  CREATE INDEX IF NOT EXISTS idx_majik_message_files_created_at
69
- ON majik_message_files(created_at);
83
+ ON ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_FILES}(created_at);
70
84
 
71
85
 
72
86
  CREATE INDEX IF NOT EXISTS idx_majik_message_files_source
73
- ON majik_message_files(source);
87
+ ON ${MAJIKAH_SQL_TABLES.MAJIK_MESSAGE_FILES}(source);
74
88
  `;
75
89
  export const MAJIKAH_SQL_SCHEMA_MAJIK_CONTACTS = `
76
- CREATE TABLE IF NOT EXISTS majik_contacts (
90
+ CREATE TABLE IF NOT EXISTS ${MAJIKAH_SQL_TABLES.MAJIK_CONTACTS} (
77
91
  id TEXT PRIMARY KEY,
78
92
  json TEXT NOT NULL,
79
93
  fingerprint TEXT,
@@ -82,11 +96,11 @@ CREATE TABLE IF NOT EXISTS majik_contacts (
82
96
  updated_at TEXT
83
97
  );
84
98
 
85
- CREATE INDEX IF NOT EXISTS idx_majik_contacts_created_at
86
- ON majik_contacts(created_at);
99
+ CREATE INDEX IF NOT EXISTS idx_majik_contacts_created_at
100
+ ON ${MAJIKAH_SQL_TABLES.MAJIK_CONTACTS}(created_at);
87
101
  `;
88
102
  export const MAJIKAH_SQL_SCHEMA_MAJIK_CONTACT_GROUPS = `
89
- CREATE TABLE IF NOT EXISTS majik_contact_groups (
103
+ CREATE TABLE IF NOT EXISTS ${MAJIKAH_SQL_TABLES.MAJIK_CONTACT_GROUPS} (
90
104
  id TEXT PRIMARY KEY,
91
105
  json TEXT NOT NULL,
92
106
  name TEXT,
@@ -95,8 +109,8 @@ CREATE TABLE IF NOT EXISTS majik_contact_groups (
95
109
  is_system INTEGER DEFAULT 0 CHECK(is_system IN (0,1))
96
110
  );
97
111
 
98
- CREATE INDEX IF NOT EXISTS idx_majik_contact_groups_created_at
99
- ON majik_contact_groups(created_at);
112
+ CREATE INDEX IF NOT EXISTS idx_majik_contact_groups_created_at
113
+ ON ${MAJIKAH_SQL_TABLES.MAJIK_CONTACT_GROUPS}(created_at);
100
114
  `;
101
115
  export const MAJIKAH_SQL_SCHEMA_FULL = buildSchemaSQL([
102
116
  MAJIKAH_SQL_SCHEMA_MAJIK_CLIENT_STATE,
@@ -1,4 +1,11 @@
1
1
  export type StorageSource = "local" | "cloud";
2
+ export interface StorageQuery<T> {
3
+ where?: Partial<T>;
4
+ limit?: number;
5
+ offset?: number;
6
+ orderBy?: keyof T;
7
+ orderDirection?: "asc" | "desc";
8
+ }
2
9
  export interface MajikStorageAdapter<T extends {
3
10
  id: string;
4
11
  }> {
@@ -11,4 +18,5 @@ export interface MajikStorageAdapter<T extends {
11
18
  exists(id: string, source?: StorageSource): Promise<boolean>;
12
19
  bulkSave(items: T[], source?: StorageSource): Promise<void>;
13
20
  bulkRemove(ids: string[], source?: StorageSource): Promise<void>;
21
+ query?(query: StorageQuery<T>, source?: StorageSource): Promise<T[]>;
14
22
  }
@@ -5,6 +5,7 @@ import type { DecryptFileOptions, EncryptFileOptions, EncryptFileResult, MAJIK_A
5
5
  import { MajikMessageChat } from "./core/database/chat/majik-message-chat";
6
6
  import { MajikMessageIdentity } from "./core/database/system/identity";
7
7
  import { MajikKey } from "@majikah/majik-key";
8
+ import { type MajikRecipient } from "@majikah/majik-envelope";
8
9
  import { MajikFile, MajikFileJSON } from "@majikah/majik-file";
9
10
  import { EnvelopeInfo, ExpectedSigner, MajikSignature, SealInfo, SealVerificationResult, SignatoriesFilter, SignatoriesResult, SignatoryInfo, type MajikSignatureJSON, type MajikSignerPublicKeys, type VerificationResult } from "@majikah/majik-signature";
10
11
  import { MajikContactManager, MajikContactManagerAdapters } from "./core/contacts/majik-contact-manager";
@@ -153,6 +154,10 @@ export declare class MajikMessage {
153
154
  hasOwnIdentity(fingerprint: string): Promise<boolean>;
154
155
  getContactByID(id: string): MajikContact | null;
155
156
  getContactByPublicKey(publicKeyBase64: string): Promise<MajikContact | null>;
157
+ getContactsByID(ids: string[], strict?: boolean): MajikContact[];
158
+ getContactsByPublicKey(publicKeys: string[]): Promise<MajikContact[]>;
159
+ getMajikRecipientsByPublicKey(publicKeys: string[], strict?: boolean): Promise<MajikRecipient[]>;
160
+ getExpectedSignersByPublicKey(publicKeys: string[], strict?: boolean): Promise<ExpectedSigner[]>;
156
161
  exportContactAsJSON(id: string): Promise<string | null>;
157
162
  exportContactAsString(id: string): Promise<string | null>;
158
163
  importContactFromJSON(jsonStr: string): Promise<MAJIK_API_RESPONSE>;
@@ -408,6 +408,22 @@ export class MajikMessage {
408
408
  return ((await this._contacts.getContactByPublicKeyBase64(publicKeyBase64)) ??
409
409
  null);
410
410
  }
411
+ getContactsByID(ids, strict = false) {
412
+ if (!ids?.length)
413
+ throw new Error("At least 1 id is required");
414
+ return this._contacts.getContactsByIds(ids, strict);
415
+ }
416
+ async getContactsByPublicKey(publicKeys) {
417
+ if (!publicKeys?.length)
418
+ throw new Error("At least 1 public key is required");
419
+ return await this._contacts.getContactsByPublicKeys(publicKeys);
420
+ }
421
+ async getMajikRecipientsByPublicKey(publicKeys, strict) {
422
+ return await this._contacts.getMajikRecipients("public_key", publicKeys, strict);
423
+ }
424
+ async getExpectedSignersByPublicKey(publicKeys, strict) {
425
+ return await this._contacts.getExpectedSigners("public_key", publicKeys, strict);
426
+ }
411
427
  async exportContactAsJSON(id) {
412
428
  if (!id?.trim())
413
429
  throw new Error("Invalid contact ID");
@@ -426,7 +442,14 @@ export class MajikMessage {
426
442
  async importContactFromString(base64Str) {
427
443
  if (!base64Str?.trim())
428
444
  throw new Error("Invalid contact string");
429
- return this._contacts.importContactFromString(base64Str);
445
+ const response = await this._contacts.importContactFromString(base64Str);
446
+ if (response.success) {
447
+ this._emit("new-contact", response.data);
448
+ }
449
+ else {
450
+ this._emit("error", response.message);
451
+ }
452
+ return response;
430
453
  }
431
454
  async exportContactCompressed(contact) {
432
455
  if (!contact?.id?.trim())
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.3.7",
5
+ "version": "0.3.9",
6
6
  "license": "Apache-2.0",
7
7
  "author": "Zelijah",
8
8
  "main": "./dist/index.js",
@@ -81,7 +81,7 @@
81
81
  "dependencies": {
82
82
  "@bokuweb/zstd-wasm": "^0.0.27",
83
83
  "@majikah/majik-contact": "^0.0.4",
84
- "@majikah/majik-envelope": "^0.0.2",
84
+ "@majikah/majik-envelope": "^0.0.4",
85
85
  "@majikah/majik-file": "^0.1.4",
86
86
  "@majikah/majik-key": "^0.2.7",
87
87
  "@majikah/majik-signature": "^0.0.17",
@@ -89,7 +89,7 @@
89
89
  "@noble/post-quantum": "^0.5.4",
90
90
  "@scure/bip39": "^1.6.0",
91
91
  "@stablelib/aes": "^2.0.1",
92
- "@stablelib/ed25519": "^2.0.2",
92
+ "@stablelib/ed25519": "^2.1.0",
93
93
  "@stablelib/gcm": "^2.0.1",
94
94
  "@stablelib/pbkdf2": "^2.0.1",
95
95
  "@stablelib/random": "^2.0.1",