@majikah/majik-message 0.1.1
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.
- package/LICENSE +67 -0
- package/README.md +559 -0
- package/dist/core/compressor/majik-compressor.d.ts +18 -0
- package/dist/core/compressor/majik-compressor.js +100 -0
- package/dist/core/contacts/majik-contact-directory.d.ts +42 -0
- package/dist/core/contacts/majik-contact-directory.js +203 -0
- package/dist/core/contacts/majik-contact.d.ts +72 -0
- package/dist/core/contacts/majik-contact.js +192 -0
- package/dist/core/crypto/constants.d.ts +8 -0
- package/dist/core/crypto/constants.js +7 -0
- package/dist/core/crypto/crypto-provider.d.ts +21 -0
- package/dist/core/crypto/crypto-provider.js +73 -0
- package/dist/core/crypto/encryption-engine.d.ts +59 -0
- package/dist/core/crypto/encryption-engine.js +257 -0
- package/dist/core/crypto/keystore.d.ts +128 -0
- package/dist/core/crypto/keystore.js +596 -0
- package/dist/core/database/chat/majik-message-chat.d.ts +117 -0
- package/dist/core/database/chat/majik-message-chat.js +513 -0
- package/dist/core/database/chat/types.d.ts +14 -0
- package/dist/core/database/chat/types.js +1 -0
- package/dist/core/database/system/identity.d.ts +61 -0
- package/dist/core/database/system/identity.js +171 -0
- package/dist/core/database/system/utils.d.ts +1 -0
- package/dist/core/database/system/utils.js +8 -0
- package/dist/core/database/thread/enums.d.ts +7 -0
- package/dist/core/database/thread/enums.js +6 -0
- package/dist/core/database/thread/mail/majik-message-mail.d.ts +177 -0
- package/dist/core/database/thread/mail/majik-message-mail.js +704 -0
- package/dist/core/database/thread/majik-message-thread.d.ts +166 -0
- package/dist/core/database/thread/majik-message-thread.js +637 -0
- package/dist/core/messages/envelope-cache.d.ts +52 -0
- package/dist/core/messages/envelope-cache.js +377 -0
- package/dist/core/messages/message-envelope.d.ts +36 -0
- package/dist/core/messages/message-envelope.js +161 -0
- package/dist/core/scanner/scanner-engine.d.ts +27 -0
- package/dist/core/scanner/scanner-engine.js +120 -0
- package/dist/core/types.d.ts +28 -0
- package/dist/core/types.js +1 -0
- package/dist/core/utils/APITranscoder.d.ts +114 -0
- package/dist/core/utils/APITranscoder.js +305 -0
- package/dist/core/utils/idb-majik-system.d.ts +15 -0
- package/dist/core/utils/idb-majik-system.js +44 -0
- package/dist/core/utils/majik-file-utils.d.ts +16 -0
- package/dist/core/utils/majik-file-utils.js +153 -0
- package/dist/core/utils/utilities.d.ts +29 -0
- package/dist/core/utils/utilities.js +94 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +18 -0
- package/dist/majik-message.d.ts +247 -0
- package/dist/majik-message.js +1221 -0
- package/package.json +104 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { init, compress as zstdCompress, decompress as zstdDecompress, } from "@bokuweb/zstd-wasm";
|
|
2
|
+
import { gzipSync, gunzipSync } from "fflate";
|
|
3
|
+
export class MajikCompressor {
|
|
4
|
+
static PREFIX = "mjkcmp";
|
|
5
|
+
static initialized = false;
|
|
6
|
+
static async ensureInit() {
|
|
7
|
+
if (!this.initialized) {
|
|
8
|
+
await init(); // only init Zstd for binary mode
|
|
9
|
+
this.initialized = true;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
static encodeInput(input) {
|
|
13
|
+
if (typeof input === "string")
|
|
14
|
+
return { type: "str", data: new TextEncoder().encode(input) };
|
|
15
|
+
if (input instanceof Uint8Array)
|
|
16
|
+
return { type: "blob", data: input };
|
|
17
|
+
if (input instanceof ArrayBuffer)
|
|
18
|
+
return { type: "blob", data: new Uint8Array(input) };
|
|
19
|
+
if (typeof input === "object")
|
|
20
|
+
return {
|
|
21
|
+
type: "json",
|
|
22
|
+
data: new TextEncoder().encode(JSON.stringify(input)),
|
|
23
|
+
};
|
|
24
|
+
throw new Error("Unsupported input type for MajikCompressor");
|
|
25
|
+
}
|
|
26
|
+
static decodeOutput(type, data) {
|
|
27
|
+
if (type === "str")
|
|
28
|
+
return new TextDecoder().decode(data);
|
|
29
|
+
if (type === "json")
|
|
30
|
+
return JSON.parse(new TextDecoder().decode(data));
|
|
31
|
+
if (type === "blob")
|
|
32
|
+
return data;
|
|
33
|
+
throw new Error(`Unsupported type for decoding: ${type}`);
|
|
34
|
+
}
|
|
35
|
+
// --- Compress input and return string ---
|
|
36
|
+
static async compress(mode, input, level = 9) {
|
|
37
|
+
const { type, data } = this.encodeInput(input);
|
|
38
|
+
let compressed;
|
|
39
|
+
if (mode === "binary") {
|
|
40
|
+
await this.ensureInit();
|
|
41
|
+
compressed = zstdCompress(data, level);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// plaintext mode → fflate gzip
|
|
45
|
+
compressed = gzipSync(data);
|
|
46
|
+
}
|
|
47
|
+
const b64 = this.uint8ArrayToBase64(compressed);
|
|
48
|
+
return `${this.PREFIX}:${type}:${b64}`;
|
|
49
|
+
}
|
|
50
|
+
// --- Decompress string with prefix ---
|
|
51
|
+
static async decompress(mode, compressedStr) {
|
|
52
|
+
if (!compressedStr.startsWith(`${this.PREFIX}:`))
|
|
53
|
+
throw new Error("Invalid MajikCompressor string format");
|
|
54
|
+
const [, type, b64] = compressedStr.split(":", 3);
|
|
55
|
+
const compressedData = this.base64ToUint8Array(b64);
|
|
56
|
+
let decompressed;
|
|
57
|
+
if (mode === "binary") {
|
|
58
|
+
await this.ensureInit();
|
|
59
|
+
decompressed = zstdDecompress(compressedData);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
decompressed = gunzipSync(compressedData);
|
|
63
|
+
}
|
|
64
|
+
return this.decodeOutput(type, decompressed);
|
|
65
|
+
}
|
|
66
|
+
static async decompressJSON(compressedStr) {
|
|
67
|
+
const result = await this.decompress("binary", compressedStr);
|
|
68
|
+
if (typeof result === "object" && !(result instanceof Uint8Array))
|
|
69
|
+
return result;
|
|
70
|
+
throw new Error("Decompressed data is not JSON");
|
|
71
|
+
}
|
|
72
|
+
static async decompressString(compressedStr) {
|
|
73
|
+
const result = await this.decompress("binary", compressedStr);
|
|
74
|
+
if (typeof result === "string")
|
|
75
|
+
return result;
|
|
76
|
+
throw new Error("Decompressed data is not a string");
|
|
77
|
+
}
|
|
78
|
+
static async decompressBlob(compressedStr) {
|
|
79
|
+
const result = await this.decompress("binary", compressedStr);
|
|
80
|
+
if (result instanceof Uint8Array)
|
|
81
|
+
return result;
|
|
82
|
+
throw new Error("Decompressed data is not a blob");
|
|
83
|
+
}
|
|
84
|
+
static uint8ArrayToBase64(u8) {
|
|
85
|
+
let binary = "";
|
|
86
|
+
const chunkSize = 0x8000;
|
|
87
|
+
for (let i = 0; i < u8.length; i += chunkSize) {
|
|
88
|
+
const chunk = u8.subarray(i, i + chunkSize);
|
|
89
|
+
binary += String.fromCharCode(...chunk);
|
|
90
|
+
}
|
|
91
|
+
return btoa(binary);
|
|
92
|
+
}
|
|
93
|
+
static base64ToUint8Array(b64) {
|
|
94
|
+
const binary = atob(b64);
|
|
95
|
+
const u8 = new Uint8Array(binary.length);
|
|
96
|
+
for (let i = 0; i < binary.length; i++)
|
|
97
|
+
u8[i] = binary.charCodeAt(i);
|
|
98
|
+
return u8;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { MessageEnvelope } from "../messages/message-envelope";
|
|
2
|
+
import { MAJIK_API_RESPONSE } from "../types";
|
|
3
|
+
import { MajikContact, MajikContactData, SerializedMajikContact } from "./majik-contact";
|
|
4
|
+
export interface MajikContactDirectoryData {
|
|
5
|
+
contacts: SerializedMajikContact[];
|
|
6
|
+
}
|
|
7
|
+
export declare class MajikContactDirectoryError extends Error {
|
|
8
|
+
cause?: unknown;
|
|
9
|
+
constructor(message: string, cause?: unknown);
|
|
10
|
+
}
|
|
11
|
+
export declare class MajikContactDirectory {
|
|
12
|
+
private contacts;
|
|
13
|
+
private fingerprintMap;
|
|
14
|
+
constructor(initialContacts?: MajikContact[]);
|
|
15
|
+
addContact(contact: MajikContact): this;
|
|
16
|
+
addContacts(contacts: MajikContact[]): this;
|
|
17
|
+
removeContact(id: string): MAJIK_API_RESPONSE;
|
|
18
|
+
updateContactMeta(id: string, meta: Partial<MajikContactData["meta"]>): MajikContact;
|
|
19
|
+
getContact(id: string): MajikContact | undefined;
|
|
20
|
+
getContactByFingerprint(fingerprint: string): MajikContact | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Get contact by public key (base64)
|
|
23
|
+
* Uses MajikContact.getPublicKeyBase64() for canonical comparison
|
|
24
|
+
*/
|
|
25
|
+
getContactByPublicKeyBase64(publicKeyBase64: string): Promise<MajikContact | undefined>;
|
|
26
|
+
hasFingerprint(fingerprint: string): boolean;
|
|
27
|
+
listContacts(sortedByLabel?: boolean, majikahOnly?: boolean): MajikContact[];
|
|
28
|
+
blockContact(id: string): MajikContact;
|
|
29
|
+
unblockContact(id: string): MajikContact;
|
|
30
|
+
hasContact(id: string): boolean;
|
|
31
|
+
clear(): this;
|
|
32
|
+
setMajikahStatus(id: string, status: boolean): MajikContact;
|
|
33
|
+
isMajikahIdentityChecked(id: string): boolean;
|
|
34
|
+
isMajikahRegistered(id: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Checks if a given envelope corresponds to a known contact
|
|
37
|
+
*/
|
|
38
|
+
hasContactForEnvelope(envelope: MessageEnvelope): boolean;
|
|
39
|
+
toJSON(): Promise<MajikContactDirectoryData>;
|
|
40
|
+
fromJSON(data: MajikContactDirectoryData): Promise<this>;
|
|
41
|
+
private assertId;
|
|
42
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { KEY_ALGO } from "../crypto/constants";
|
|
2
|
+
import { base64ToArrayBuffer } from "../utils/utilities";
|
|
3
|
+
import { MajikContact, } from "./majik-contact";
|
|
4
|
+
/* -------------------------------
|
|
5
|
+
* Errors
|
|
6
|
+
* ------------------------------- */
|
|
7
|
+
export class MajikContactDirectoryError extends Error {
|
|
8
|
+
cause;
|
|
9
|
+
constructor(message, cause) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "MajikContactDirectoryError";
|
|
12
|
+
this.cause = cause;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/* -------------------------------
|
|
16
|
+
* MajikContactDirectory Class
|
|
17
|
+
* ------------------------------- */
|
|
18
|
+
export class MajikContactDirectory {
|
|
19
|
+
contacts = new Map();
|
|
20
|
+
fingerprintMap = new Map(); // fingerprint → contact id
|
|
21
|
+
constructor(initialContacts) {
|
|
22
|
+
if (initialContacts?.length) {
|
|
23
|
+
initialContacts.forEach((c) => this.addContact(c));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/* ================================
|
|
27
|
+
* Contact Management
|
|
28
|
+
* ================================ */
|
|
29
|
+
addContact(contact) {
|
|
30
|
+
if (!(contact instanceof MajikContact)) {
|
|
31
|
+
throw new MajikContactDirectoryError("Invalid contact instance");
|
|
32
|
+
}
|
|
33
|
+
if (this.contacts.has(contact.id)) {
|
|
34
|
+
throw new MajikContactDirectoryError(`Contact with id "${contact.id}" already exists`);
|
|
35
|
+
}
|
|
36
|
+
this.contacts.set(contact.id, contact);
|
|
37
|
+
this.fingerprintMap.set(contact.fingerprint, contact.id);
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
addContacts(contacts) {
|
|
41
|
+
contacts.forEach((c) => this.addContact(c));
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
removeContact(id) {
|
|
45
|
+
this.assertId(id);
|
|
46
|
+
const contact = this.contacts.get(id);
|
|
47
|
+
if (contact) {
|
|
48
|
+
this.fingerprintMap.delete(contact.fingerprint);
|
|
49
|
+
this.contacts.delete(id);
|
|
50
|
+
return {
|
|
51
|
+
message: "Contact removed successfully",
|
|
52
|
+
success: true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
return {
|
|
57
|
+
message: "Contact not found",
|
|
58
|
+
success: false,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
updateContactMeta(id, meta) {
|
|
63
|
+
const contact = this.getContact(id);
|
|
64
|
+
if (!contact)
|
|
65
|
+
throw new MajikContactDirectoryError("Contact not found");
|
|
66
|
+
if (meta) {
|
|
67
|
+
meta.label && contact.updateLabel(meta.label);
|
|
68
|
+
meta.notes && contact.updateNotes(meta.notes);
|
|
69
|
+
meta.blocked !== undefined && contact.setBlocked(meta.blocked);
|
|
70
|
+
}
|
|
71
|
+
return contact;
|
|
72
|
+
}
|
|
73
|
+
getContact(id) {
|
|
74
|
+
this.assertId(id);
|
|
75
|
+
return this.contacts.get(id);
|
|
76
|
+
}
|
|
77
|
+
getContactByFingerprint(fingerprint) {
|
|
78
|
+
if (!fingerprint) {
|
|
79
|
+
throw new MajikContactDirectoryError("Fingerprint must be a non-empty string");
|
|
80
|
+
}
|
|
81
|
+
const contactId = this.fingerprintMap.get(fingerprint);
|
|
82
|
+
return contactId ? this.contacts.get(contactId) : undefined;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get contact by public key (base64)
|
|
86
|
+
* Uses MajikContact.getPublicKeyBase64() for canonical comparison
|
|
87
|
+
*/
|
|
88
|
+
async getContactByPublicKeyBase64(publicKeyBase64) {
|
|
89
|
+
if (!publicKeyBase64 || typeof publicKeyBase64 !== "string") {
|
|
90
|
+
throw new MajikContactDirectoryError("Public key must be a non-empty base64 string");
|
|
91
|
+
}
|
|
92
|
+
for (const contact of this.contacts.values()) {
|
|
93
|
+
const contactKey = await contact.getPublicKeyBase64();
|
|
94
|
+
if (contactKey === publicKeyBase64) {
|
|
95
|
+
return contact;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
hasFingerprint(fingerprint) {
|
|
101
|
+
return this.fingerprintMap.has(fingerprint);
|
|
102
|
+
}
|
|
103
|
+
listContacts(sortedByLabel = false, majikahOnly = false) {
|
|
104
|
+
let contacts = [...this.contacts.values()];
|
|
105
|
+
if (majikahOnly) {
|
|
106
|
+
contacts = contacts.filter((c) => c.isMajikahRegistered());
|
|
107
|
+
}
|
|
108
|
+
if (sortedByLabel) {
|
|
109
|
+
contacts.sort((a, b) => (a.meta.label || "").localeCompare(b.meta.label || ""));
|
|
110
|
+
}
|
|
111
|
+
return contacts;
|
|
112
|
+
}
|
|
113
|
+
blockContact(id) {
|
|
114
|
+
const contact = this.getContact(id);
|
|
115
|
+
if (!contact)
|
|
116
|
+
throw new MajikContactDirectoryError(`Contact with id "${id}" not found for block`);
|
|
117
|
+
return contact.block();
|
|
118
|
+
}
|
|
119
|
+
unblockContact(id) {
|
|
120
|
+
const contact = this.getContact(id);
|
|
121
|
+
if (!contact)
|
|
122
|
+
throw new MajikContactDirectoryError(`Contact with id "${id}" not found for unblock`);
|
|
123
|
+
return contact.unblock();
|
|
124
|
+
}
|
|
125
|
+
hasContact(id) {
|
|
126
|
+
return this.contacts.has(id);
|
|
127
|
+
}
|
|
128
|
+
clear() {
|
|
129
|
+
this.contacts.clear();
|
|
130
|
+
this.fingerprintMap.clear();
|
|
131
|
+
return this;
|
|
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
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Checks if a given envelope corresponds to a known contact
|
|
154
|
+
*/
|
|
155
|
+
hasContactForEnvelope(envelope) {
|
|
156
|
+
try {
|
|
157
|
+
const fingerprint = envelope.extractFingerprint();
|
|
158
|
+
return this.hasFingerprint(fingerprint);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/* ================================
|
|
165
|
+
* Serialization / Persistence
|
|
166
|
+
* ================================ */
|
|
167
|
+
async toJSON() {
|
|
168
|
+
const contactsData = [];
|
|
169
|
+
for (const contact of this.contacts.values()) {
|
|
170
|
+
contactsData.push(await contact.toJSON());
|
|
171
|
+
}
|
|
172
|
+
return { contacts: contactsData };
|
|
173
|
+
}
|
|
174
|
+
async fromJSON(data) {
|
|
175
|
+
if (!data?.contacts) {
|
|
176
|
+
throw new MajikContactDirectoryError("Invalid serialized data");
|
|
177
|
+
}
|
|
178
|
+
this.clear();
|
|
179
|
+
for (const item of data.contacts) {
|
|
180
|
+
const raw = base64ToArrayBuffer(item.publicKeyBase64);
|
|
181
|
+
let publicKey;
|
|
182
|
+
try {
|
|
183
|
+
publicKey = await crypto.subtle.importKey("raw", raw, KEY_ALGO, true, []);
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
// Fallback: create a raw-key wrapper when the browser does not support the namedCurve
|
|
187
|
+
publicKey = { raw: new Uint8Array(raw) };
|
|
188
|
+
}
|
|
189
|
+
const contact = MajikContact.create(item.id, publicKey, item.fingerprint, item.meta);
|
|
190
|
+
this.contacts.set(contact.id, contact);
|
|
191
|
+
this.fingerprintMap.set(contact.fingerprint, contact.id);
|
|
192
|
+
}
|
|
193
|
+
return this;
|
|
194
|
+
}
|
|
195
|
+
/* ================================
|
|
196
|
+
* Validation Helpers
|
|
197
|
+
* ================================ */
|
|
198
|
+
assertId(id) {
|
|
199
|
+
if (!id || typeof id !== "string") {
|
|
200
|
+
throw new MajikContactDirectoryError("Contact ID must be a non-empty string");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { MajikMessageIdentityJSON } from "../database/system/identity";
|
|
2
|
+
import { ISODateString } from "../types";
|
|
3
|
+
export type SerializedMajikContact = Omit<MajikContactData, "publicKey"> & {
|
|
4
|
+
publicKeyBase64: string;
|
|
5
|
+
};
|
|
6
|
+
export interface MajikContactMeta {
|
|
7
|
+
label?: string;
|
|
8
|
+
notes?: string;
|
|
9
|
+
blocked?: boolean;
|
|
10
|
+
createdAt?: ISODateString;
|
|
11
|
+
updatedAt?: ISODateString;
|
|
12
|
+
}
|
|
13
|
+
export interface MajikContactData {
|
|
14
|
+
id: string;
|
|
15
|
+
publicKey: CryptoKey | {
|
|
16
|
+
raw: Uint8Array;
|
|
17
|
+
};
|
|
18
|
+
fingerprint: string;
|
|
19
|
+
meta?: MajikContactMeta;
|
|
20
|
+
majikah_registered?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface MajikContactCard {
|
|
23
|
+
id: string;
|
|
24
|
+
publicKey: string;
|
|
25
|
+
fingerprint: string;
|
|
26
|
+
label: string;
|
|
27
|
+
}
|
|
28
|
+
export declare class MajikContactError extends Error {
|
|
29
|
+
cause?: unknown;
|
|
30
|
+
constructor(message: string, cause?: unknown);
|
|
31
|
+
}
|
|
32
|
+
export declare class MajikContact {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly publicKey: CryptoKey | {
|
|
35
|
+
raw: Uint8Array;
|
|
36
|
+
};
|
|
37
|
+
readonly fingerprint: string;
|
|
38
|
+
meta: MajikContactMeta;
|
|
39
|
+
private majikah_registered?;
|
|
40
|
+
constructor(data: MajikContactData);
|
|
41
|
+
static create(id: string, publicKey: CryptoKey | {
|
|
42
|
+
raw: Uint8Array;
|
|
43
|
+
}, fingerprint: string, meta?: Partial<MajikContactMeta>): MajikContact;
|
|
44
|
+
private assertId;
|
|
45
|
+
private assertPublicKey;
|
|
46
|
+
private assertFingerprint;
|
|
47
|
+
private updateTimestamp;
|
|
48
|
+
updateLabel(label: string): this;
|
|
49
|
+
updateNotes(notes: string): this;
|
|
50
|
+
isBlocked(): boolean;
|
|
51
|
+
setBlocked(blocked: boolean): this;
|
|
52
|
+
block(): this;
|
|
53
|
+
unblock(): this;
|
|
54
|
+
isMajikahIdentityChecked(): boolean;
|
|
55
|
+
isMajikahRegistered(): boolean;
|
|
56
|
+
setMajikahStatus(status: boolean): this;
|
|
57
|
+
getDisplayName(): Promise<string>;
|
|
58
|
+
/**
|
|
59
|
+
* Support both CryptoKey and raw-key wrappers (fallbacks when WebCrypto X25519 unsupported)
|
|
60
|
+
*/
|
|
61
|
+
getPublicKeyBase64(): Promise<string>;
|
|
62
|
+
toJSON(): Promise<SerializedMajikContact>;
|
|
63
|
+
/**
|
|
64
|
+
* Reconstruct a MajikContact from its serialized form
|
|
65
|
+
*/
|
|
66
|
+
static fromJSON(serialized: SerializedMajikContact): MajikContact;
|
|
67
|
+
/**
|
|
68
|
+
* Create a new MajikContact from a MajikMessageIdentityJSON
|
|
69
|
+
*/
|
|
70
|
+
static fromIdentityJSON(identityJSON: MajikMessageIdentityJSON): Promise<MajikContact>;
|
|
71
|
+
static isBlocked(contact: MajikContact): boolean;
|
|
72
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { arrayBufferToBase64, base64ToArrayBuffer } from "../utils/utilities";
|
|
2
|
+
/* -------------------------------
|
|
3
|
+
* Errors
|
|
4
|
+
* ------------------------------- */
|
|
5
|
+
export class MajikContactError extends Error {
|
|
6
|
+
cause;
|
|
7
|
+
constructor(message, cause) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "MajikContactError";
|
|
10
|
+
this.cause = cause;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/* -------------------------------
|
|
14
|
+
* MajikContact Class
|
|
15
|
+
* ------------------------------- */
|
|
16
|
+
export class MajikContact {
|
|
17
|
+
id;
|
|
18
|
+
publicKey;
|
|
19
|
+
fingerprint;
|
|
20
|
+
meta;
|
|
21
|
+
majikah_registered;
|
|
22
|
+
constructor(data) {
|
|
23
|
+
this.assertId(data.id);
|
|
24
|
+
this.assertPublicKey(data.publicKey);
|
|
25
|
+
this.assertFingerprint(data.fingerprint);
|
|
26
|
+
this.id = data.id;
|
|
27
|
+
this.publicKey = data.publicKey;
|
|
28
|
+
this.fingerprint = data.fingerprint;
|
|
29
|
+
this.meta = {
|
|
30
|
+
label: data.meta?.label || "",
|
|
31
|
+
notes: data.meta?.notes || "",
|
|
32
|
+
blocked: data.meta?.blocked || false,
|
|
33
|
+
createdAt: data.meta?.createdAt || new Date().toISOString(),
|
|
34
|
+
updatedAt: data.meta?.updatedAt || new Date().toISOString(),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
static create(id, publicKey, fingerprint, meta) {
|
|
38
|
+
return new MajikContact({
|
|
39
|
+
id,
|
|
40
|
+
publicKey,
|
|
41
|
+
fingerprint,
|
|
42
|
+
meta,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
assertId(id) {
|
|
46
|
+
if (!id || typeof id !== "string") {
|
|
47
|
+
throw new MajikContactError("Contact ID must be a non-empty string");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
assertPublicKey(key) {
|
|
51
|
+
// Accept either a WebCrypto CryptoKey (with .type === 'public')
|
|
52
|
+
// or a raw-key wrapper object that contains a Uint8Array `raw` field.
|
|
53
|
+
if (!key)
|
|
54
|
+
throw new MajikContactError("Invalid public key");
|
|
55
|
+
const anyKey = key;
|
|
56
|
+
if (anyKey && typeof anyKey === "object") {
|
|
57
|
+
if (anyKey.type === "public")
|
|
58
|
+
return;
|
|
59
|
+
if (anyKey.raw instanceof Uint8Array)
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
throw new MajikContactError("Invalid public key");
|
|
63
|
+
}
|
|
64
|
+
assertFingerprint(fingerprint) {
|
|
65
|
+
if (!fingerprint || typeof fingerprint !== "string") {
|
|
66
|
+
throw new MajikContactError("Fingerprint must be a non-empty string");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
updateTimestamp() {
|
|
70
|
+
this.meta.updatedAt = new Date().toISOString();
|
|
71
|
+
}
|
|
72
|
+
updateLabel(label) {
|
|
73
|
+
if (typeof label !== "string")
|
|
74
|
+
throw new MajikContactError("Label must be a string");
|
|
75
|
+
this.meta.label = label;
|
|
76
|
+
this.updateTimestamp();
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
updateNotes(notes) {
|
|
80
|
+
if (typeof notes !== "string")
|
|
81
|
+
throw new MajikContactError("Notes must be a string");
|
|
82
|
+
this.meta.notes = notes;
|
|
83
|
+
this.updateTimestamp();
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
isBlocked() {
|
|
87
|
+
return this.meta.blocked || false;
|
|
88
|
+
}
|
|
89
|
+
setBlocked(blocked) {
|
|
90
|
+
if (typeof blocked !== "boolean")
|
|
91
|
+
throw new MajikContactError("Blocked must be boolean");
|
|
92
|
+
this.meta.blocked = blocked;
|
|
93
|
+
this.updateTimestamp();
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
// Idempotent block/unblock for safe scanning
|
|
97
|
+
block() {
|
|
98
|
+
if (!this.isBlocked())
|
|
99
|
+
this.setBlocked(true);
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
unblock() {
|
|
103
|
+
if (this.isBlocked())
|
|
104
|
+
this.setBlocked(false);
|
|
105
|
+
return this;
|
|
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
|
+
}
|
|
117
|
+
async getDisplayName() {
|
|
118
|
+
return this.meta.label || (await this.getPublicKeyBase64());
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Support both CryptoKey and raw-key wrappers (fallbacks when WebCrypto X25519 unsupported)
|
|
122
|
+
*/
|
|
123
|
+
async getPublicKeyBase64() {
|
|
124
|
+
try {
|
|
125
|
+
// If it's a CryptoKey, export with SubtleCrypto
|
|
126
|
+
const raw = await crypto.subtle.exportKey("raw", this.publicKey);
|
|
127
|
+
return arrayBufferToBase64(raw);
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
// Fallback: publicKey may be a wrapper with `raw` Uint8Array
|
|
131
|
+
const maybe = this.publicKey;
|
|
132
|
+
if (maybe && maybe.raw instanceof Uint8Array) {
|
|
133
|
+
return arrayBufferToBase64(maybe.raw.buffer);
|
|
134
|
+
}
|
|
135
|
+
throw e;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async toJSON() {
|
|
139
|
+
return {
|
|
140
|
+
id: this.id,
|
|
141
|
+
fingerprint: this.fingerprint,
|
|
142
|
+
meta: { ...this.meta },
|
|
143
|
+
publicKeyBase64: await this.getPublicKeyBase64(),
|
|
144
|
+
majikah_registered: this.majikah_registered,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Reconstruct a MajikContact from its serialized form
|
|
149
|
+
*/
|
|
150
|
+
static fromJSON(serialized) {
|
|
151
|
+
try {
|
|
152
|
+
const publicKeyRaw = new Uint8Array(base64ToArrayBuffer(serialized.publicKeyBase64));
|
|
153
|
+
return new MajikContact({
|
|
154
|
+
id: serialized.id,
|
|
155
|
+
fingerprint: serialized.fingerprint,
|
|
156
|
+
meta: serialized.meta,
|
|
157
|
+
publicKey: { raw: publicKeyRaw },
|
|
158
|
+
majikah_registered: serialized.majikah_registered,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
throw new MajikContactError("Failed to deserialize MajikContact", err);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Create a new MajikContact from a MajikMessageIdentityJSON
|
|
167
|
+
*/
|
|
168
|
+
static async fromIdentityJSON(identityJSON) {
|
|
169
|
+
try {
|
|
170
|
+
const publicKeyRaw = new Uint8Array(base64ToArrayBuffer(identityJSON.public_key));
|
|
171
|
+
const contactData = {
|
|
172
|
+
id: identityJSON.id,
|
|
173
|
+
publicKey: { raw: publicKeyRaw },
|
|
174
|
+
fingerprint: identityJSON.id,
|
|
175
|
+
meta: {
|
|
176
|
+
label: identityJSON.label,
|
|
177
|
+
createdAt: identityJSON.timestamp,
|
|
178
|
+
updatedAt: identityJSON.timestamp,
|
|
179
|
+
blocked: identityJSON.restricted,
|
|
180
|
+
},
|
|
181
|
+
majikah_registered: true,
|
|
182
|
+
};
|
|
183
|
+
return new MajikContact(contactData);
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
throw new MajikContactError("Failed to create MajikContact from MajikMessageIdentityJSON", err);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
static isBlocked(contact) {
|
|
190
|
+
return !!contact.meta.blocked;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { MAJIK_API_RESPONSE } from "../types";
|
|
2
|
+
export declare const KEY_ALGO: {
|
|
3
|
+
readonly name: "ECDH";
|
|
4
|
+
readonly namedCurve: "X25519";
|
|
5
|
+
};
|
|
6
|
+
export declare const MAJIK_SALT = "MajikMessageSalt";
|
|
7
|
+
export declare const MAJIK_MNEMONIC_SALT = "MajikMessageMnemonicSalt";
|
|
8
|
+
export declare const API_DEFAULT_FAIL: MAJIK_API_RESPONSE;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const KEY_ALGO = { name: "ECDH", namedCurve: "X25519" };
|
|
2
|
+
export const MAJIK_SALT = "MajikMessageSalt";
|
|
3
|
+
export const MAJIK_MNEMONIC_SALT = "MajikMessageMnemonicSalt";
|
|
4
|
+
export const API_DEFAULT_FAIL = {
|
|
5
|
+
message: "Something went wrong",
|
|
6
|
+
success: false,
|
|
7
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const IV_LENGTH = 12;
|
|
2
|
+
export declare function generateRandomBytes(len: number): Uint8Array;
|
|
3
|
+
export declare function generateEd25519Keypair(): {
|
|
4
|
+
edPublic: Uint8Array;
|
|
5
|
+
edSecret: Uint8Array;
|
|
6
|
+
xPublic: Uint8Array | null;
|
|
7
|
+
xSecret: Uint8Array | null;
|
|
8
|
+
};
|
|
9
|
+
export declare function deriveEd25519FromSeed(seed32: Uint8Array): {
|
|
10
|
+
edPublic: Uint8Array;
|
|
11
|
+
edSecret: Uint8Array;
|
|
12
|
+
xPublic: Uint8Array | null;
|
|
13
|
+
xSecret: Uint8Array | null;
|
|
14
|
+
};
|
|
15
|
+
export declare function fingerprintFromPublicRaw(rawPublic: Uint8Array): string;
|
|
16
|
+
export declare function aesGcmEncrypt(keyBytes: Uint8Array, iv: Uint8Array, plaintext: Uint8Array): Uint8Array;
|
|
17
|
+
export declare function aesGcmDecrypt(keyBytes: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array): Uint8Array | null;
|
|
18
|
+
export declare function deriveKeyFromPassphrase(passphrase: string, salt: Uint8Array, iterations?: number, keyLen?: number): Uint8Array;
|
|
19
|
+
export declare function deriveKeyFromMnemonic(mnemonic: string, salt: Uint8Array, iterations?: number, keyLen?: number): Uint8Array;
|
|
20
|
+
export declare function x25519SharedSecret(privRaw: Uint8Array, pubRaw: Uint8Array): Uint8Array;
|
|
21
|
+
export declare function sha256(input: string): string;
|