@thezelijah/majik-message 1.0.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.
Files changed (35) hide show
  1. package/LICENSE +67 -0
  2. package/README.md +265 -0
  3. package/dist/core/contacts/majik-contact-directory.d.ts +34 -0
  4. package/dist/core/contacts/majik-contact-directory.js +165 -0
  5. package/dist/core/contacts/majik-contact.d.ts +53 -0
  6. package/dist/core/contacts/majik-contact.js +135 -0
  7. package/dist/core/crypto/constants.d.ts +7 -0
  8. package/dist/core/crypto/constants.js +6 -0
  9. package/dist/core/crypto/crypto-provider.d.ts +20 -0
  10. package/dist/core/crypto/crypto-provider.js +70 -0
  11. package/dist/core/crypto/encryption-engine.d.ts +59 -0
  12. package/dist/core/crypto/encryption-engine.js +257 -0
  13. package/dist/core/crypto/keystore.d.ts +126 -0
  14. package/dist/core/crypto/keystore.js +575 -0
  15. package/dist/core/messages/envelope-cache.d.ts +51 -0
  16. package/dist/core/messages/envelope-cache.js +375 -0
  17. package/dist/core/messages/message-envelope.d.ts +36 -0
  18. package/dist/core/messages/message-envelope.js +161 -0
  19. package/dist/core/scanner/scanner-engine.d.ts +27 -0
  20. package/dist/core/scanner/scanner-engine.js +120 -0
  21. package/dist/core/types.d.ts +23 -0
  22. package/dist/core/types.js +1 -0
  23. package/dist/core/utils/APITranscoder.d.ts +114 -0
  24. package/dist/core/utils/APITranscoder.js +305 -0
  25. package/dist/core/utils/idb-majik-system.d.ts +15 -0
  26. package/dist/core/utils/idb-majik-system.js +37 -0
  27. package/dist/core/utils/majik-file-utils.d.ts +16 -0
  28. package/dist/core/utils/majik-file-utils.js +153 -0
  29. package/dist/core/utils/utilities.d.ts +22 -0
  30. package/dist/core/utils/utilities.js +80 -0
  31. package/dist/index.d.ts +13 -0
  32. package/dist/index.js +12 -0
  33. package/dist/majik-message.d.ts +202 -0
  34. package/dist/majik-message.js +940 -0
  35. package/package.json +97 -0
@@ -0,0 +1,375 @@
1
+ import { arrayBufferToBase64, base64ToArrayBuffer } from "../utils/utilities";
2
+ import { MessageEnvelope } from "./message-envelope";
3
+ /* -------------------------------
4
+ * Errors
5
+ * ------------------------------- */
6
+ export class EnvelopeCacheError extends Error {
7
+ cause;
8
+ constructor(message, cause) {
9
+ super(message);
10
+ this.name = "EnvelopeCacheError";
11
+ this.cause = cause;
12
+ }
13
+ }
14
+ /* -------------------------------
15
+ * EnvelopeCache
16
+ * ------------------------------- */
17
+ export class EnvelopeCache {
18
+ dbPromise;
19
+ dbName;
20
+ storeName;
21
+ maxEntries;
22
+ // In-memory cache
23
+ memoryCache;
24
+ memoryCacheSize;
25
+ constructor(config) {
26
+ this.dbName = config?.dbName || "MajikEnvelopeDB";
27
+ this.storeName = config?.storeName || "envelopes";
28
+ this.maxEntries = config?.maxEntries;
29
+ this.memoryCacheSize = config?.memoryCacheSize || 100;
30
+ this.memoryCache = new Map();
31
+ this.dbPromise = this.initDB();
32
+ }
33
+ /* -------------------------------
34
+ * IndexedDB Initialization
35
+ * ------------------------------- */
36
+ initDB() {
37
+ return new Promise((resolve, reject) => {
38
+ const request = indexedDB.open(this.dbName, 1);
39
+ request.onupgradeneeded = (event) => {
40
+ const db = event.target.result;
41
+ if (!db.objectStoreNames.contains(this.storeName)) {
42
+ const store = db.createObjectStore(this.storeName, { keyPath: "id" });
43
+ store.createIndex("timestamp", "timestamp");
44
+ }
45
+ };
46
+ request.onsuccess = () => resolve(request.result);
47
+ request.onerror = () => reject(new EnvelopeCacheError("Failed to open IndexedDB", request.error));
48
+ });
49
+ }
50
+ /* -------------------------------
51
+ * Generate unique ID for envelope (SHA-256)
52
+ * ------------------------------- */
53
+ async getEnvelopeId(envelope) {
54
+ const hashBuffer = await crypto.subtle.digest("SHA-256", envelope.encryptedBlob);
55
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
56
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
57
+ }
58
+ /* -------------------------------
59
+ * Save envelope to cache (IDB + memory)
60
+ * -------------------------------
61
+ */
62
+ async set(envelope, source) {
63
+ const id = await this.getEnvelopeId(envelope);
64
+ try {
65
+ // Add to memory cache
66
+ this.memoryCache.set(id, envelope);
67
+ if (this.memoryCache.size > this.memoryCacheSize) {
68
+ // Remove oldest entry
69
+ const firstKey = this.memoryCache.keys().next().value;
70
+ if (firstKey !== undefined) {
71
+ this.memoryCache.delete(firstKey);
72
+ }
73
+ }
74
+ // Persist to IndexedDB
75
+ const db = await this.dbPromise;
76
+ const tx = db.transaction(this.storeName, "readwrite");
77
+ const store = tx.objectStore(this.storeName);
78
+ const timestamp = Date.now();
79
+ await new Promise((resolve, reject) => {
80
+ const req = store.put({ id, envelope, timestamp, source });
81
+ req.onsuccess = () => resolve();
82
+ req.onerror = () => reject(new EnvelopeCacheError("Failed to save envelope", req.error));
83
+ });
84
+ // Optional: enforce maxEntries in IDB
85
+ if (this.maxEntries)
86
+ await this.enforceMaxEntries();
87
+ }
88
+ catch (err) {
89
+ console.error("EnvelopeCache set error:", err);
90
+ throw err;
91
+ }
92
+ }
93
+ /* -------------------------------
94
+ * List recent envelopes with pagination (most recent first)
95
+ * Returns array of objects: { id, envelope, timestamp, source }
96
+ * -------------------------------
97
+ */
98
+ async listRecent(offset = 0, limit = 50) {
99
+ const results = [];
100
+ try {
101
+ const db = await this.dbPromise;
102
+ const tx = db.transaction(this.storeName, "readonly");
103
+ const store = tx.objectStore(this.storeName);
104
+ const index = store.index("timestamp");
105
+ // Iterate newest first
106
+ const req = index.openCursor(null, "prev");
107
+ let skipped = 0;
108
+ await new Promise((resolve, reject) => {
109
+ req.onsuccess = (event) => {
110
+ const cursor = event.target
111
+ .result;
112
+ if (!cursor)
113
+ return resolve();
114
+ if (skipped < offset) {
115
+ skipped++;
116
+ cursor.continue();
117
+ return;
118
+ }
119
+ if (results.length < limit) {
120
+ const val = cursor.value;
121
+ const envelope = new MessageEnvelope(val.envelope.encryptedBlob); // wrap here
122
+ results.push({
123
+ id: val.id,
124
+ envelope: envelope,
125
+ timestamp: val.timestamp,
126
+ source: val.source,
127
+ });
128
+ cursor.continue();
129
+ }
130
+ else {
131
+ resolve();
132
+ }
133
+ };
134
+ req.onerror = () => reject(new EnvelopeCacheError("Failed to list recent envelopes", req.error));
135
+ });
136
+ }
137
+ catch (err) {
138
+ console.error("EnvelopeCache listRecent error:", err);
139
+ }
140
+ return results;
141
+ }
142
+ /* -------------------------------
143
+ * Get envelope (checks memory first)
144
+ * ------------------------------- */
145
+ async get(envelope) {
146
+ const id = await this.getEnvelopeId(envelope);
147
+ // 1️⃣ Try memory cache
148
+ if (this.memoryCache.has(id))
149
+ return this.memoryCache.get(id);
150
+ // 2️⃣ Fallback to IndexedDB
151
+ const dbEnvelope = await this.getById(id);
152
+ if (dbEnvelope) {
153
+ // Populate memory cache
154
+ this.memoryCache.set(id, dbEnvelope);
155
+ if (this.memoryCache.size > this.memoryCacheSize) {
156
+ const firstKey = this.memoryCache.keys().next().value;
157
+ if (firstKey !== undefined) {
158
+ this.memoryCache.delete(firstKey);
159
+ }
160
+ }
161
+ }
162
+ return dbEnvelope;
163
+ }
164
+ /* -------------------------------
165
+ * Get envelope by ID directly
166
+ * ------------------------------- */
167
+ async getById(id) {
168
+ try {
169
+ const db = await this.dbPromise;
170
+ const tx = db.transaction(this.storeName, "readonly");
171
+ const store = tx.objectStore(this.storeName);
172
+ return await new Promise((resolve, reject) => {
173
+ const req = store.get(id);
174
+ req.onsuccess = () => resolve(req.result?.envelope);
175
+ req.onerror = () => reject(new EnvelopeCacheError("Failed to get envelope", req.error));
176
+ });
177
+ }
178
+ catch (err) {
179
+ console.error("EnvelopeCache getById error:", err);
180
+ return undefined;
181
+ }
182
+ }
183
+ /* -------------------------------
184
+ * Check if envelope exists
185
+ * ------------------------------- */
186
+ async has(envelope) {
187
+ const id = await this.getEnvelopeId(envelope);
188
+ if (this.memoryCache.has(id))
189
+ return true;
190
+ try {
191
+ const db = await this.dbPromise;
192
+ const tx = db.transaction(this.storeName, "readonly");
193
+ const store = tx.objectStore(this.storeName);
194
+ return await new Promise((resolve) => {
195
+ const req = store.getKey(id);
196
+ req.onsuccess = () => resolve(req.result !== undefined);
197
+ req.onerror = () => resolve(false);
198
+ });
199
+ }
200
+ catch (err) {
201
+ console.error("EnvelopeCache has error:", err);
202
+ return false;
203
+ }
204
+ }
205
+ /* -------------------------------
206
+ * Delete envelope (memory + IDB)
207
+ * ------------------------------- */
208
+ async delete(envelope) {
209
+ const id = await this.getEnvelopeId(envelope);
210
+ // Remove from memory
211
+ this.memoryCache.delete(id);
212
+ try {
213
+ const db = await this.dbPromise;
214
+ const tx = db.transaction(this.storeName, "readwrite");
215
+ const store = tx.objectStore(this.storeName);
216
+ await new Promise((resolve, reject) => {
217
+ const req = store.delete(id);
218
+ req.onsuccess = () => resolve();
219
+ req.onerror = () => reject(new EnvelopeCacheError("Failed to delete envelope", req.error));
220
+ });
221
+ }
222
+ catch (err) {
223
+ console.error("EnvelopeCache delete error:", err);
224
+ throw err;
225
+ }
226
+ }
227
+ /* -------------------------------
228
+ * Delete all envelopes by fingerprint
229
+ * ------------------------------- */
230
+ async deleteByFingerprint(fingerprint) {
231
+ try {
232
+ const db = await this.dbPromise;
233
+ const tx = db.transaction(this.storeName, "readwrite");
234
+ const store = tx.objectStore(this.storeName);
235
+ // First, collect all IDs to delete
236
+ const idsToDelete = [];
237
+ const cursorReq = store.openCursor();
238
+ await new Promise((resolve, reject) => {
239
+ cursorReq.onsuccess = (event) => {
240
+ const cursor = event.target
241
+ .result;
242
+ if (!cursor)
243
+ return resolve();
244
+ const val = cursor.value;
245
+ const env = new MessageEnvelope(val.envelope.encryptedBlob);
246
+ const envelopeFingerprint = env.extractFingerprint();
247
+ if (envelopeFingerprint === fingerprint) {
248
+ idsToDelete.push(val.id);
249
+ }
250
+ cursor.continue();
251
+ };
252
+ cursorReq.onerror = () => reject(new EnvelopeCacheError("Failed to iterate envelopes", cursorReq.error));
253
+ });
254
+ // Delete from IndexedDB and memory cache
255
+ for (const id of idsToDelete) {
256
+ await new Promise((resolve, reject) => {
257
+ const req = store.delete(id);
258
+ req.onsuccess = () => resolve();
259
+ req.onerror = () => reject(new EnvelopeCacheError("Failed to delete envelope", req.error));
260
+ });
261
+ // Remove from memory cache
262
+ this.memoryCache.delete(id);
263
+ }
264
+ }
265
+ catch (err) {
266
+ console.error("EnvelopeCache deleteByFingerprint error:", err);
267
+ throw err;
268
+ }
269
+ }
270
+ /* -------------------------------
271
+ * Clear all envelopes (memory + IDB)
272
+ * ------------------------------- */
273
+ async clear() {
274
+ // Clear memory cache
275
+ this.memoryCache.clear();
276
+ try {
277
+ const db = await this.dbPromise;
278
+ const tx = db.transaction(this.storeName, "readwrite");
279
+ const store = tx.objectStore(this.storeName);
280
+ await new Promise((resolve, reject) => {
281
+ const req = store.clear();
282
+ req.onsuccess = () => resolve();
283
+ req.onerror = () => reject(new EnvelopeCacheError("Failed to clear envelope cache", req.error));
284
+ });
285
+ }
286
+ catch (err) {
287
+ console.error("EnvelopeCache clear error:", err);
288
+ return {
289
+ message: "Failed to clear envelope cache",
290
+ success: false,
291
+ };
292
+ }
293
+ return {
294
+ success: true,
295
+ message: "Envelope cache cleared successfully",
296
+ };
297
+ }
298
+ /* -------------------------------
299
+ * Enforce max entries in IDB (oldest first)
300
+ * ------------------------------- */
301
+ async enforceMaxEntries() {
302
+ if (!this.maxEntries)
303
+ return;
304
+ try {
305
+ const db = await this.dbPromise;
306
+ const tx = db.transaction(this.storeName, "readwrite");
307
+ const store = tx.objectStore(this.storeName);
308
+ const index = store.index("timestamp");
309
+ const countReq = store.count();
310
+ const total = await new Promise((resolve, reject) => {
311
+ countReq.onsuccess = () => resolve(countReq.result);
312
+ countReq.onerror = () => reject(countReq.error);
313
+ });
314
+ if (total <= this.maxEntries)
315
+ return;
316
+ const toDelete = total - this.maxEntries;
317
+ const keys = [];
318
+ const cursorReq = index.openKeyCursor();
319
+ await new Promise((resolve, reject) => {
320
+ cursorReq.onsuccess = (event) => {
321
+ const cursor = event.target
322
+ .result;
323
+ if (cursor && keys.length < toDelete) {
324
+ keys.push(cursor.primaryKey);
325
+ cursor.continue();
326
+ }
327
+ else {
328
+ resolve();
329
+ }
330
+ };
331
+ cursorReq.onerror = () => reject(cursorReq.error);
332
+ });
333
+ for (const key of keys) {
334
+ await new Promise((resolve, reject) => {
335
+ const req = store.delete(key);
336
+ req.onsuccess = () => resolve();
337
+ req.onerror = () => reject(req.error);
338
+ });
339
+ }
340
+ }
341
+ catch (err) {
342
+ console.error("EnvelopeCache enforceMaxEntries error:", err);
343
+ }
344
+ }
345
+ toJSON() {
346
+ const items = [];
347
+ this.memoryCache.forEach((envelope, id) => {
348
+ items.push({
349
+ id,
350
+ base64Payload: arrayBufferToBase64(envelope.encryptedBlob),
351
+ timestamp: Date.now(),
352
+ });
353
+ });
354
+ return {
355
+ config: {
356
+ dbName: this.dbName,
357
+ storeName: this.storeName,
358
+ maxEntries: this.maxEntries,
359
+ memoryCacheSize: this.memoryCacheSize,
360
+ },
361
+ items,
362
+ };
363
+ }
364
+ /* -------------------------------
365
+ * Deserialize cache from JSON
366
+ * ------------------------------- */
367
+ static fromJSON(json) {
368
+ const cache = new EnvelopeCache(json.config);
369
+ for (const item of json.items) {
370
+ const envelope = new MessageEnvelope(base64ToArrayBuffer(item.base64Payload));
371
+ cache.memoryCache.set(item.id, envelope);
372
+ }
373
+ return cache;
374
+ }
375
+ }
@@ -0,0 +1,36 @@
1
+ import type { EnvelopePayload, RecipientKeys, SingleRecipientPayload } from "../types";
2
+ export type EnvelopeErrorCode = "INVALID_INPUT" | "FORMAT_ERROR" | "VALIDATION_ERROR";
3
+ export declare class MessageEnvelopeError extends Error {
4
+ readonly code: EnvelopeErrorCode;
5
+ readonly raw?: string;
6
+ constructor(code: EnvelopeErrorCode, message: string, raw?: string);
7
+ }
8
+ export declare class MessageEnvelope {
9
+ /** Raw decoded encrypted payload */
10
+ readonly encryptedBlob: ArrayBuffer;
11
+ static readonly PREFIX = "~*$MJKMSG";
12
+ static readonly DEFAULT_FINGERPRINT_LENGTH = 32;
13
+ constructor(blob: ArrayBuffer);
14
+ get raw(): ArrayBuffer;
15
+ /** Quick validation without throwing */
16
+ static tryFromString(raw: unknown): {
17
+ envelope?: MessageEnvelope;
18
+ error?: MessageEnvelopeError;
19
+ };
20
+ static isEnvelopeCandidate(input: unknown): boolean;
21
+ getVersion(): number;
22
+ static fromMatchedString(raw: unknown): MessageEnvelope;
23
+ extractFingerprint(fingerprintLength?: number): string;
24
+ extractEncryptedPayload(): EnvelopePayload;
25
+ /**
26
+ * Returns the ephemeral encrypted key for a given fingerprint
27
+ */
28
+ getRecipientKey(fingerprint: string): RecipientKeys | undefined;
29
+ getSingleRecipientPayload(): SingleRecipientPayload | undefined;
30
+ /**
31
+ * Checks if this envelope contains a key for the given fingerprint
32
+ */
33
+ hasRecipient(fingerprint: string): boolean;
34
+ isGroup(): boolean;
35
+ isSolo(): boolean;
36
+ }
@@ -0,0 +1,161 @@
1
+ import { base64ToArrayBuffer, arrayBufferToBase64 } from "../utils/utilities";
2
+ /* -------------------------------
3
+ * Constants
4
+ * ------------------------------- */
5
+ const ENVELOPE_PREFIX = "~*$MJKMSG";
6
+ const ENVELOPE_REGEX = /^~\*\$MJKMSG:([A-Za-z0-9+/=]+)$/;
7
+ const MAX_ENVELOPE_LENGTH = 16_384; // raw string
8
+ const MAX_PAYLOAD_BYTES = 12_288; // decoded binary
9
+ export class MessageEnvelopeError extends Error {
10
+ code;
11
+ raw;
12
+ constructor(code, message, raw) {
13
+ super(message);
14
+ this.name = "MessageEnvelopeError";
15
+ this.code = code;
16
+ this.raw = raw;
17
+ }
18
+ }
19
+ /* -------------------------------
20
+ * MessageEnvelope
21
+ * ------------------------------- */
22
+ export class MessageEnvelope {
23
+ /** Raw decoded encrypted payload */
24
+ encryptedBlob;
25
+ static PREFIX = ENVELOPE_PREFIX;
26
+ static DEFAULT_FINGERPRINT_LENGTH = 32;
27
+ constructor(blob) {
28
+ this.encryptedBlob = blob;
29
+ }
30
+ get raw() {
31
+ return this.encryptedBlob;
32
+ }
33
+ /** Quick validation without throwing */
34
+ static tryFromString(raw) {
35
+ try {
36
+ return { envelope: MessageEnvelope.fromMatchedString(raw) };
37
+ }
38
+ catch (err) {
39
+ return { error: err };
40
+ }
41
+ }
42
+ static isEnvelopeCandidate(input) {
43
+ return (typeof input === "string" &&
44
+ input.length <= MAX_ENVELOPE_LENGTH &&
45
+ ENVELOPE_REGEX.test(input.trim()));
46
+ }
47
+ getVersion() {
48
+ const view = new Uint8Array(this.encryptedBlob);
49
+ if (view.length < 1) {
50
+ throw new MessageEnvelopeError("VALIDATION_ERROR", "Encrypted blob too short to contain version byte");
51
+ }
52
+ return view[0];
53
+ }
54
+ /* -------------------------------
55
+ * Factory
56
+ * ------------------------------- */
57
+ static fromMatchedString(raw) {
58
+ if (typeof raw !== "string") {
59
+ throw new MessageEnvelopeError("INVALID_INPUT", "Envelope input must be a string");
60
+ }
61
+ const trimmed = raw.trim();
62
+ if (trimmed.length > MAX_ENVELOPE_LENGTH) {
63
+ throw new MessageEnvelopeError("VALIDATION_ERROR", "Envelope exceeds maximum allowed length", raw);
64
+ }
65
+ const match = ENVELOPE_REGEX.exec(trimmed);
66
+ if (!match) {
67
+ throw new MessageEnvelopeError("FORMAT_ERROR", `Invalid envelope format. Expected ${ENVELOPE_PREFIX}:<base64>`, raw);
68
+ }
69
+ let decoded;
70
+ try {
71
+ decoded = base64ToArrayBuffer(match[1]);
72
+ }
73
+ catch {
74
+ throw new MessageEnvelopeError("VALIDATION_ERROR", "Base64 payload failed to decode", raw);
75
+ }
76
+ if (decoded.byteLength === 0) {
77
+ throw new MessageEnvelopeError("VALIDATION_ERROR", "Decoded payload is empty", raw);
78
+ }
79
+ if (decoded.byteLength > MAX_PAYLOAD_BYTES) {
80
+ throw new MessageEnvelopeError("VALIDATION_ERROR", "Decoded payload exceeds size limit", raw);
81
+ }
82
+ return new MessageEnvelope(decoded);
83
+ }
84
+ /* -------------------------------
85
+ * Extract Fingerprint (first one)
86
+ * ------------------------------- */
87
+ extractFingerprint(fingerprintLength = MessageEnvelope.DEFAULT_FINGERPRINT_LENGTH) {
88
+ const view = new Uint8Array(this.encryptedBlob);
89
+ if (view.length < 1 + fingerprintLength) {
90
+ throw new MessageEnvelopeError("VALIDATION_ERROR", "Encrypted blob too short to contain fingerprint");
91
+ }
92
+ const fingerprintBytes = view.slice(1, 1 + fingerprintLength);
93
+ const ab = fingerprintBytes.buffer.slice(fingerprintBytes.byteOffset, fingerprintBytes.byteOffset + fingerprintBytes.length);
94
+ return arrayBufferToBase64(ab);
95
+ }
96
+ /* -------------------------------
97
+ * Extract Encrypted Payload
98
+ * ------------------------------- */
99
+ extractEncryptedPayload() {
100
+ const view = new Uint8Array(this.encryptedBlob);
101
+ const versionLength = 1;
102
+ const fingerprintLength = MessageEnvelope.DEFAULT_FINGERPRINT_LENGTH;
103
+ if (view.length < versionLength + fingerprintLength + 1) {
104
+ throw new MessageEnvelopeError("VALIDATION_ERROR", "Encrypted blob too short to contain payload");
105
+ }
106
+ const payloadBytes = view.slice(versionLength + fingerprintLength);
107
+ const payloadText = new TextDecoder().decode(payloadBytes);
108
+ let parsed;
109
+ try {
110
+ parsed = JSON.parse(payloadText);
111
+ }
112
+ catch {
113
+ throw new MessageEnvelopeError("VALIDATION_ERROR", "Failed to parse encrypted payload JSON");
114
+ }
115
+ // Validate multi-recipient
116
+ if ("keys" in parsed) {
117
+ if (!parsed.iv ||
118
+ !parsed.ciphertext ||
119
+ !parsed.ephemeralPublicKey ||
120
+ !Array.isArray(parsed.keys)) {
121
+ throw new MessageEnvelopeError("VALIDATION_ERROR", "Multi-recipient payload missing required fields");
122
+ }
123
+ for (const k of parsed.keys) {
124
+ if (!k.fingerprint || !k.ephemeralEncryptedKey || !k.nonce) {
125
+ throw new MessageEnvelopeError("VALIDATION_ERROR", "Invalid key entry in multi-recipient payload");
126
+ }
127
+ }
128
+ }
129
+ return parsed;
130
+ }
131
+ /* -------------------------------
132
+ * Multi-Recipient Helpers
133
+ * ------------------------------- */
134
+ /**
135
+ * Returns the ephemeral encrypted key for a given fingerprint
136
+ */
137
+ getRecipientKey(fingerprint) {
138
+ const payload = this.extractEncryptedPayload();
139
+ if ("keys" in payload) {
140
+ return payload.keys.find((k) => k.fingerprint === fingerprint);
141
+ }
142
+ return undefined;
143
+ }
144
+ getSingleRecipientPayload() {
145
+ const payload = this.extractEncryptedPayload();
146
+ return "keys" in payload ? undefined : payload;
147
+ }
148
+ /**
149
+ * Checks if this envelope contains a key for the given fingerprint
150
+ */
151
+ hasRecipient(fingerprint) {
152
+ return !!this.getRecipientKey(fingerprint);
153
+ }
154
+ isGroup() {
155
+ const payload = this.extractEncryptedPayload();
156
+ return "keys" in payload;
157
+ }
158
+ isSolo() {
159
+ return !this.isGroup();
160
+ }
161
+ }
@@ -0,0 +1,27 @@
1
+ import { MessageEnvelope } from "../messages/message-envelope";
2
+ import { MajikContactDirectory } from "../contacts/majik-contact-directory";
3
+ import { EnvelopeCache } from "../messages/envelope-cache";
4
+ interface ScannerEngineConfig {
5
+ contactDirectory: MajikContactDirectory;
6
+ envelopeCache?: EnvelopeCache;
7
+ onEnvelopeFound?: (envelope: MessageEnvelope) => void;
8
+ onUntrusted?: (raw: string) => void;
9
+ onError?: (err: Error, context?: {
10
+ raw?: string;
11
+ }) => void;
12
+ }
13
+ export declare class ScannerEngine {
14
+ private observer?;
15
+ private contactDirectory;
16
+ private envelopeCache?;
17
+ private onEnvelopeFound?;
18
+ private onUntrusted?;
19
+ private onError?;
20
+ private processedNodes;
21
+ constructor(config: ScannerEngineConfig);
22
+ scanText(text: string): Promise<void>;
23
+ scanDOM(rootNode: Node): void;
24
+ startDOMObserver(rootNode: Node): void;
25
+ stopDOMObserver(): void;
26
+ }
27
+ export {};