@majikah/majik-message 0.2.3 → 0.2.4
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/dist/core/contacts/majik-contact.d.ts +8 -0
- package/dist/core/contacts/majik-contact.js +8 -0
- package/dist/core/crypto/keystore.d.ts +0 -18
- package/dist/core/crypto/keystore.js +0 -18
- package/dist/core/types.d.ts +11 -0
- package/dist/majik-message.d.ts +368 -44
- package/dist/majik-message.js +621 -69
- package/package.json +5 -4
|
@@ -7,6 +7,8 @@ export type SerializedMajikContact = {
|
|
|
7
7
|
publicKeyBase64: string;
|
|
8
8
|
mlKey: string;
|
|
9
9
|
majikah_registered?: boolean;
|
|
10
|
+
edPublicKeyBase64?: string;
|
|
11
|
+
mlDsaPublicKeyBase64?: string;
|
|
10
12
|
};
|
|
11
13
|
export interface MajikContactMeta {
|
|
12
14
|
label?: string;
|
|
@@ -24,6 +26,8 @@ export interface MajikContactData {
|
|
|
24
26
|
mlKey: string;
|
|
25
27
|
meta?: MajikContactMeta;
|
|
26
28
|
majikah_registered?: boolean;
|
|
29
|
+
edPublicKeyBase64?: string;
|
|
30
|
+
mlDsaPublicKeyBase64?: string;
|
|
27
31
|
}
|
|
28
32
|
export interface MajikContactCard {
|
|
29
33
|
id: string;
|
|
@@ -31,6 +35,8 @@ export interface MajikContactCard {
|
|
|
31
35
|
fingerprint: string;
|
|
32
36
|
label: string;
|
|
33
37
|
mlKey: string;
|
|
38
|
+
edPublicKeyBase64?: string;
|
|
39
|
+
mlDsaPublicKeyBase64?: string;
|
|
34
40
|
}
|
|
35
41
|
export declare class MajikContactError extends Error {
|
|
36
42
|
cause?: unknown;
|
|
@@ -43,6 +49,8 @@ export declare class MajikContact {
|
|
|
43
49
|
};
|
|
44
50
|
readonly fingerprint: string;
|
|
45
51
|
readonly mlKey: string;
|
|
52
|
+
readonly edPublicKeyBase64: string;
|
|
53
|
+
readonly mlDsaPublicKeyBase64: string;
|
|
46
54
|
meta: MajikContactMeta;
|
|
47
55
|
private majikah_registered?;
|
|
48
56
|
constructor(data: MajikContactData);
|
|
@@ -18,6 +18,8 @@ export class MajikContact {
|
|
|
18
18
|
publicKey;
|
|
19
19
|
fingerprint;
|
|
20
20
|
mlKey;
|
|
21
|
+
edPublicKeyBase64;
|
|
22
|
+
mlDsaPublicKeyBase64;
|
|
21
23
|
meta;
|
|
22
24
|
majikah_registered;
|
|
23
25
|
constructor(data) {
|
|
@@ -29,6 +31,8 @@ export class MajikContact {
|
|
|
29
31
|
this.publicKey = data.publicKey;
|
|
30
32
|
this.fingerprint = data.fingerprint;
|
|
31
33
|
this.mlKey = data.mlKey;
|
|
34
|
+
this.edPublicKeyBase64 = data.edPublicKeyBase64 || "";
|
|
35
|
+
this.mlDsaPublicKeyBase64 = data.mlDsaPublicKeyBase64 || "";
|
|
32
36
|
this.meta = {
|
|
33
37
|
label: data.meta?.label || "",
|
|
34
38
|
notes: data.meta?.notes || "",
|
|
@@ -152,6 +156,8 @@ export class MajikContact {
|
|
|
152
156
|
publicKeyBase64: await this.getPublicKeyBase64(),
|
|
153
157
|
majikah_registered: this.majikah_registered,
|
|
154
158
|
mlKey: this.mlKey,
|
|
159
|
+
edPublicKeyBase64: this.edPublicKeyBase64,
|
|
160
|
+
mlDsaPublicKeyBase64: this.mlDsaPublicKeyBase64,
|
|
155
161
|
};
|
|
156
162
|
}
|
|
157
163
|
/**
|
|
@@ -167,6 +173,8 @@ export class MajikContact {
|
|
|
167
173
|
publicKey: { raw: publicKeyRaw },
|
|
168
174
|
majikah_registered: serialized.majikah_registered,
|
|
169
175
|
mlKey: serialized.mlKey,
|
|
176
|
+
edPublicKeyBase64: serialized.edPublicKeyBase64,
|
|
177
|
+
mlDsaPublicKeyBase64: serialized.mlDsaPublicKeyBase64,
|
|
170
178
|
});
|
|
171
179
|
}
|
|
172
180
|
catch (err) {
|
|
@@ -4,24 +4,6 @@
|
|
|
4
4
|
* IDB persistence + in-memory cache layer for MajikKey accounts.
|
|
5
5
|
* Replaces MajikKeyStore as the account storage backend for MajikMessage.
|
|
6
6
|
*
|
|
7
|
-
* Design:
|
|
8
|
-
* - MajikKeyJSON is the canonical storage format (replaces MajikKeyStore.SerializedIdentity)
|
|
9
|
-
* - MajikKey is the canonical account object (replaces KeyStoreIdentity)
|
|
10
|
-
* - In-memory Map<id, MajikKey> caches unlocked accounts for the session
|
|
11
|
-
* - All crypto (KDF, encrypt/decrypt) stays inside MajikKey — this class only does IDB
|
|
12
|
-
*
|
|
13
|
-
* Migration from MajikKeyStore:
|
|
14
|
-
* Old accounts stored as SerializedIdentity (5 fields, PBKDF2) are loaded via
|
|
15
|
-
* fromSerializedIdentity() which reconstructs a locked MajikKey from the legacy format.
|
|
16
|
-
* On next unlock, MajikKey automatically uses the correct KDF (PBKDF2 for old accounts).
|
|
17
|
-
* On next passphrase change or importFromMnemonicBackup(), the account is fully upgraded.
|
|
18
|
-
*
|
|
19
|
-
* IDB schema:
|
|
20
|
-
* Store name: "majik-keys" (separate from old "identities" store — intentional)
|
|
21
|
-
* Key path: "id"
|
|
22
|
-
* Value: MajikKeyJSON (full MajikKey serialization)
|
|
23
|
-
*
|
|
24
|
-
* Legacy store: "identities" (MajikKeyStore format — read-only migration path)
|
|
25
7
|
*/
|
|
26
8
|
import { MajikKey, SerializedIdentity } from "@majikah/majik-key";
|
|
27
9
|
/**
|
|
@@ -4,24 +4,6 @@
|
|
|
4
4
|
* IDB persistence + in-memory cache layer for MajikKey accounts.
|
|
5
5
|
* Replaces MajikKeyStore as the account storage backend for MajikMessage.
|
|
6
6
|
*
|
|
7
|
-
* Design:
|
|
8
|
-
* - MajikKeyJSON is the canonical storage format (replaces MajikKeyStore.SerializedIdentity)
|
|
9
|
-
* - MajikKey is the canonical account object (replaces KeyStoreIdentity)
|
|
10
|
-
* - In-memory Map<id, MajikKey> caches unlocked accounts for the session
|
|
11
|
-
* - All crypto (KDF, encrypt/decrypt) stays inside MajikKey — this class only does IDB
|
|
12
|
-
*
|
|
13
|
-
* Migration from MajikKeyStore:
|
|
14
|
-
* Old accounts stored as SerializedIdentity (5 fields, PBKDF2) are loaded via
|
|
15
|
-
* fromSerializedIdentity() which reconstructs a locked MajikKey from the legacy format.
|
|
16
|
-
* On next unlock, MajikKey automatically uses the correct KDF (PBKDF2 for old accounts).
|
|
17
|
-
* On next passphrase change or importFromMnemonicBackup(), the account is fully upgraded.
|
|
18
|
-
*
|
|
19
|
-
* IDB schema:
|
|
20
|
-
* Store name: "majik-keys" (separate from old "identities" store — intentional)
|
|
21
|
-
* Key path: "id"
|
|
22
|
-
* Value: MajikKeyJSON (full MajikKey serialization)
|
|
23
|
-
*
|
|
24
|
-
* Legacy store: "identities" (MajikKeyStore format — read-only migration path)
|
|
25
7
|
*/
|
|
26
8
|
import { MajikKey } from "@majikah/majik-key";
|
|
27
9
|
import { base64ToArrayBuffer } from "../utils/utilities";
|
package/dist/core/types.d.ts
CHANGED
|
@@ -196,6 +196,10 @@ export interface EncryptFileResult {
|
|
|
196
196
|
* .mjkb Blob for R2 upload. Equivalent to file.toMJKB().
|
|
197
197
|
*/
|
|
198
198
|
binary: Blob;
|
|
199
|
+
/**
|
|
200
|
+
* Signed .mjkb Blob for offline or direct sharing. Equivalent to file.toSignedMJKB().
|
|
201
|
+
*/
|
|
202
|
+
signedBinary: Blob;
|
|
199
203
|
}
|
|
200
204
|
/**
|
|
201
205
|
* Options for MajikMessage.decryptFile().
|
|
@@ -213,4 +217,11 @@ export interface DecryptFileOptions {
|
|
|
213
217
|
* automatically — you rarely need to set this explicitly.
|
|
214
218
|
*/
|
|
215
219
|
accountId?: string;
|
|
220
|
+
/**
|
|
221
|
+
* The MajikFileJSON metadata row from Supabase.
|
|
222
|
+
* When provided, the signature field is automatically threaded into
|
|
223
|
+
* decryptWithMetadata() so the returned signature is populated without
|
|
224
|
+
* a second parse or round-trip.
|
|
225
|
+
*/
|
|
226
|
+
metadata?: MajikFileJSON;
|
|
216
227
|
}
|
package/dist/majik-message.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ import { MajikContactDirectory, type MajikContactDirectoryData } from "./core/co
|
|
|
6
6
|
import type { DecryptFileOptions, EncryptFileOptions, EncryptFileResult, MAJIK_API_RESPONSE, MajikMessagePublicKey } from "./core/types";
|
|
7
7
|
import { MajikMessageChat } from "./core/database/chat/majik-message-chat";
|
|
8
8
|
import { MajikMessageIdentity } from "./core/database/system/identity";
|
|
9
|
+
import { MajikKey } from "@majikah/majik-key";
|
|
10
|
+
import { MajikFile, MajikFileJSON } from "@majikah/majik-file";
|
|
11
|
+
import { MajikSignature, type MajikSignatureJSON, type MajikSignerPublicKeys, type VerificationResult } from "@majikah/majik-signature";
|
|
9
12
|
type MajikMessageEvents = "message" | "envelope" | "untrusted" | "error" | "new-account" | "new-contact" | "removed-account" | "removed-contact" | "active-account-change";
|
|
10
13
|
interface MajikMessageStatic<T extends MajikMessage> {
|
|
11
14
|
new (config: MajikMessageConfig, id?: string): T;
|
|
@@ -161,13 +164,14 @@ export declare class MajikMessage {
|
|
|
161
164
|
decryptMajikMessageChat(encryptedPayload: string, recipientId?: string): Promise<string>;
|
|
162
165
|
/**
|
|
163
166
|
* Encrypt a binary file and return everything the caller needs to persist it.
|
|
164
|
-
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* @throws MajikFileError on validation failures or crypto errors (re-thrown
|
|
168
|
-
* from MajikFile.create() so the caller gets typed errors).
|
|
167
|
+
* Automatically signs the encrypted .mjkb binary using the active account's
|
|
168
|
+
* signing keys if available. Falls back to unsigned encryption for legacy
|
|
169
|
+
* accounts that pre-date signing key support.
|
|
169
170
|
*
|
|
170
|
-
* @
|
|
171
|
+
* @throws Error if no active account or a recipient cannot be resolved.
|
|
172
|
+
* @throws MajikFileError on validation or crypto failures (typed, re-thrown).
|
|
173
|
+
*
|
|
174
|
+
* @example — self-encrypted user upload, auto-signed
|
|
171
175
|
* ```ts
|
|
172
176
|
* const result = await majik.encryptFile({
|
|
173
177
|
* data: fileBytes,
|
|
@@ -176,64 +180,228 @@ export declare class MajikMessage {
|
|
|
176
180
|
* });
|
|
177
181
|
* await r2.put(result.metadata.r2_key, result.binary);
|
|
178
182
|
* await supabase.from("majik_files").insert(result.metadata);
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* @example — group chat image
|
|
182
|
-
* ```ts
|
|
183
|
-
* const result = await majik.encryptFile({
|
|
184
|
-
* data: imageBytes,
|
|
185
|
-
* context: "chat_image",
|
|
186
|
-
* originalName: "photo.png",
|
|
187
|
-
* conversationId: "conv_abc123",
|
|
188
|
-
* recipientIds: ["contact_id_1", "contact_id_2"],
|
|
189
|
-
* isTemporary: true,
|
|
190
|
-
* expiresAt: MajikFile.buildExpiryDate(15),
|
|
191
|
-
* });
|
|
183
|
+
* // result.metadata.signature is populated if the account has signing keys
|
|
192
184
|
* ```
|
|
193
185
|
*/
|
|
194
186
|
encryptFile(options: EncryptFileOptions): Promise<EncryptFileResult>;
|
|
195
187
|
/**
|
|
196
188
|
* Decrypt a .mjkb binary and return the original raw bytes.
|
|
197
189
|
*
|
|
190
|
+
* When `metadata` is provided, the signature field is automatically
|
|
191
|
+
* threaded through so callers receive the deserialized MajikSignature
|
|
192
|
+
* in the result without any extra work. Verify it with verifyMajikFile()
|
|
193
|
+
* or MajikSignature.verify() after decryption.
|
|
194
|
+
*
|
|
198
195
|
* Flow:
|
|
199
196
|
* 1. If `accountId` is provided, that account is tried first.
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* This mirrors the behaviour of decryptEnvelope() for group messages.
|
|
204
|
-
* 3. Delegates to MajikFile.decrypt() — which handles:
|
|
205
|
-
* • .mjkb binary parsing and magic-byte validation
|
|
206
|
-
* • Single vs group payload discrimination
|
|
207
|
-
* • ML-KEM decapsulation
|
|
208
|
-
* • AES-256-GCM decryption
|
|
209
|
-
* • Zstd decompression (if the file was compressed)
|
|
210
|
-
*
|
|
211
|
-
* @returns Raw plaintext bytes — the original file content before encryption.
|
|
197
|
+
* 2. For group files, every own account is tried in sequence.
|
|
198
|
+
* 3. Delegates to MajikFile.decryptWithMetadata() for binary parsing,
|
|
199
|
+
* ML-KEM decapsulation, AES-256-GCM decryption, and decompression.
|
|
212
200
|
*
|
|
213
|
-
* @
|
|
214
|
-
*
|
|
215
|
-
* errors — callers can import MajikFileError for typed catch blocks.
|
|
201
|
+
* @returns Raw plaintext bytes, original filename, MIME type, and
|
|
202
|
+
* deserialized MajikSignature (null if unsigned or no metadata).
|
|
216
203
|
*
|
|
217
|
-
* @
|
|
218
|
-
*
|
|
219
|
-
* const mjkbBlob = await r2.get(metadata.r2_key);
|
|
220
|
-
* const rawBytes = await majik.decryptFile({ source: mjkbBlob });
|
|
221
|
-
* const url = URL.createObjectURL(new Blob([rawBytes], { type: metadata.mime_type }));
|
|
222
|
-
* ```
|
|
204
|
+
* @throws Error if no own account can decrypt the file.
|
|
205
|
+
* @throws MajikFileError on corrupt binary, wrong key, or format errors.
|
|
223
206
|
*
|
|
224
|
-
* @example —
|
|
207
|
+
* @example — basic usage with metadata row from Supabase
|
|
225
208
|
* ```ts
|
|
226
|
-
* const
|
|
227
|
-
*
|
|
228
|
-
*
|
|
209
|
+
* const mjkbBlob = await r2.get(row.r2_key);
|
|
210
|
+
* const { bytes, mimeType, signature } = await majik.decryptFile({
|
|
211
|
+
* source: mjkbBlob,
|
|
212
|
+
* metadata: row,
|
|
229
213
|
* });
|
|
214
|
+
* if (signature) {
|
|
215
|
+
* const result = await majik.verifyMajikFile(file, { contactId: row.user_id });
|
|
216
|
+
* }
|
|
230
217
|
* ```
|
|
231
218
|
*/
|
|
232
219
|
decryptFile(options: DecryptFileOptions): Promise<{
|
|
233
220
|
bytes: Uint8Array;
|
|
234
221
|
originalName: string | null;
|
|
235
222
|
mimeType: string | null;
|
|
223
|
+
signature: MajikSignature | null;
|
|
236
224
|
}>;
|
|
225
|
+
/**
|
|
226
|
+
* Sign an already-created MajikFile using the active (or specified) account
|
|
227
|
+
* and attach the signature to the instance.
|
|
228
|
+
*
|
|
229
|
+
* Use this for deferred signing — when a file was created via create() and
|
|
230
|
+
* signing happens on a second pass (e.g. after user confirmation in the UI).
|
|
231
|
+
* For create + sign in one call, use encryptFile() which calls createAndSign().
|
|
232
|
+
*
|
|
233
|
+
* The file's binary must be loaded (_binary !== null).
|
|
234
|
+
* Call file.toJSON() and persist to Supabase after signing to save the signature.
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* await majik.signMajikFile(file);
|
|
238
|
+
* await supabase
|
|
239
|
+
* .from("majik_files")
|
|
240
|
+
* .update({ signature: file.signatureRaw, last_update: file.lastUpdate })
|
|
241
|
+
* .eq("id", file.id);
|
|
242
|
+
*/
|
|
243
|
+
signMajikFile(file: MajikFile, options?: {
|
|
244
|
+
accountId?: string;
|
|
245
|
+
contentType?: string;
|
|
246
|
+
timestamp?: string;
|
|
247
|
+
}): Promise<MajikSignature>;
|
|
248
|
+
/**
|
|
249
|
+
* Verify the signature attached to a MajikFile.
|
|
250
|
+
*
|
|
251
|
+
* The file's binary must be loaded — call file.attachBinary(r2Bytes) first
|
|
252
|
+
* if the instance was restored from a metadata-only Supabase row.
|
|
253
|
+
*
|
|
254
|
+
* Signer resolution:
|
|
255
|
+
* - contactId: looked up in the contact directory (own accounts included)
|
|
256
|
+
* - publicKeyBase64: looked up via contact directory
|
|
257
|
+
* - key: used directly (skips directory lookup)
|
|
258
|
+
* - none provided: falls back to public keys embedded in the signature
|
|
259
|
+
* envelope (self-reported — always cross-check result.signerId)
|
|
260
|
+
*
|
|
261
|
+
* Returns null if the file has no signature.
|
|
262
|
+
*
|
|
263
|
+
* @example — verify against the file's owner contact
|
|
264
|
+
* file.attachBinary(await r2.get(row.r2_key).arrayBuffer());
|
|
265
|
+
* const result = await majik.verifyMajikFile(file, {
|
|
266
|
+
* contactId: ownerContactId,
|
|
267
|
+
* });
|
|
268
|
+
* if (result?.valid) console.log("Verified, signed by", result.signerId);
|
|
269
|
+
*/
|
|
270
|
+
verifyMajikFile(file: MajikFile, options?: {
|
|
271
|
+
contactId?: string;
|
|
272
|
+
publicKeyBase64?: string;
|
|
273
|
+
key?: MajikKey;
|
|
274
|
+
}): Promise<VerificationResult | null>;
|
|
275
|
+
/**
|
|
276
|
+
* Full binary verification of a MajikFile — decrypts first, then verifies
|
|
277
|
+
* the signature against the recovered plaintext bytes.
|
|
278
|
+
*
|
|
279
|
+
* Stronger than verifyMajikFile() because it proves both:
|
|
280
|
+
* 1. The ciphertext decrypts correctly (AES-GCM auth tag passes)
|
|
281
|
+
* 2. The plaintext matches what the signer originally signed
|
|
282
|
+
*
|
|
283
|
+
* Requires both a decryption identity (own account) and the signer's
|
|
284
|
+
* public keys. The binary must be loaded.
|
|
285
|
+
*
|
|
286
|
+
* @param decryptAccountId Which own account to use for decryption.
|
|
287
|
+
* Defaults to the active account.
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* const result = await majik.verifyMajikFileBinary(file, {
|
|
291
|
+
* contactId: "contact_abc",
|
|
292
|
+
* });
|
|
293
|
+
* if (result.valid) console.log("Plaintext verified");
|
|
294
|
+
*/
|
|
295
|
+
verifyMajikFileBinary(file: MajikFile, options?: {
|
|
296
|
+
contactId?: string;
|
|
297
|
+
publicKeyBase64?: string;
|
|
298
|
+
key?: MajikKey;
|
|
299
|
+
decryptAccountId?: string;
|
|
300
|
+
}): Promise<VerificationResult>;
|
|
301
|
+
/**
|
|
302
|
+
* Check whether the active (or specified) account is the signer of a
|
|
303
|
+
* MajikFile by comparing fingerprints.
|
|
304
|
+
*
|
|
305
|
+
* This is a fast, synchronous fingerprint comparison — it does NOT
|
|
306
|
+
* cryptographically verify the signature. Use verifyMajikFile() for proof.
|
|
307
|
+
*
|
|
308
|
+
* Useful for gating UI actions:
|
|
309
|
+
* - Show "Re-sign" button only if the active user is the signer
|
|
310
|
+
* - Show "Signed by you" vs "Signed by [contact]" labels
|
|
311
|
+
*
|
|
312
|
+
* @returns true if the account's fingerprint matches the envelope's signerId.
|
|
313
|
+
* false if the file is unsigned, the account has no signing keys,
|
|
314
|
+
* the account is not in the keystore memory cache, or fingerprints
|
|
315
|
+
* don't match.
|
|
316
|
+
*
|
|
317
|
+
* @example
|
|
318
|
+
* if (majik.isActiveAccountSigner(file)) {
|
|
319
|
+
* showResignButton();
|
|
320
|
+
* }
|
|
321
|
+
*/
|
|
322
|
+
isActiveAccountSigner(file: MajikFile, accountId?: string): boolean;
|
|
323
|
+
/**
|
|
324
|
+
* Return a rich metadata object describing who signed a MajikFile,
|
|
325
|
+
* without performing cryptographic verification.
|
|
326
|
+
*
|
|
327
|
+
* Combines getSignatureInfo() with a contact directory and keystore lookup
|
|
328
|
+
* so the UI can show a human-readable label (e.g. "Signed by Alice") instead
|
|
329
|
+
* of a raw fingerprint, and can distinguish own-account signatures from
|
|
330
|
+
* external ones.
|
|
331
|
+
*
|
|
332
|
+
* Synchronous — reads only local state. Call verifyMajikFile() separately
|
|
333
|
+
* if cryptographic proof is required.
|
|
334
|
+
*
|
|
335
|
+
* @returns null if the file is unsigned or the signature is malformed.
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* const info = majik.getMajikFileSignerInfo(file);
|
|
339
|
+
* if (info) {
|
|
340
|
+
* console.log(info.isOwnAccount ? "Signed by you" : `Signed by ${info.signerLabel}`);
|
|
341
|
+
* console.log("at", info.timestamp);
|
|
342
|
+
* }
|
|
343
|
+
*/
|
|
344
|
+
getMajikFileSignerInfo(file: MajikFile): {
|
|
345
|
+
signerId: string;
|
|
346
|
+
timestamp: string;
|
|
347
|
+
contentType?: string;
|
|
348
|
+
contentHash: string;
|
|
349
|
+
/** Human-readable contact label if the signer is in the contact directory. */
|
|
350
|
+
signerLabel: string | null;
|
|
351
|
+
/** True if the signer is one of your own accounts. */
|
|
352
|
+
isOwnAccount: boolean;
|
|
353
|
+
/** True if the signer is in the contact directory (own or external). */
|
|
354
|
+
isKnownContact: boolean;
|
|
355
|
+
} | null;
|
|
356
|
+
/**
|
|
357
|
+
* Remove the signature from a MajikFile and persist the change.
|
|
358
|
+
*
|
|
359
|
+
* A convenience wrapper around file.removeSignature() that handles the
|
|
360
|
+
* Supabase update in one call. Useful for admin flows or when re-signing
|
|
361
|
+
* after a file mutation.
|
|
362
|
+
*
|
|
363
|
+
* Unlike file.removeSignature() which only mutates the in-memory instance,
|
|
364
|
+
* this method also returns the updated metadata row ready for upsert.
|
|
365
|
+
*
|
|
366
|
+
* Note: removing a signature does not re-encrypt or modify the R2 binary —
|
|
367
|
+
* only the Supabase metadata row changes.
|
|
368
|
+
*
|
|
369
|
+
* @returns The updated MajikFileJSON with signature: null.
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* const updatedRow = majik.unsignMajikFile(file);
|
|
373
|
+
* await supabase
|
|
374
|
+
* .from("majik_files")
|
|
375
|
+
* .update({ signature: null, last_update: updatedRow.last_update })
|
|
376
|
+
* .eq("id", file.id);
|
|
377
|
+
*/
|
|
378
|
+
unsignMajikFile(file: MajikFile): MajikFileJSON;
|
|
379
|
+
/**
|
|
380
|
+
* Re-sign a MajikFile — removes any existing signature, then signs
|
|
381
|
+
* with the active (or specified) account.
|
|
382
|
+
*
|
|
383
|
+
* Idempotent: calling this multiple times always produces a fresh signature
|
|
384
|
+
* from the specified account. Useful after a contact label change or when
|
|
385
|
+
* rotating signing keys.
|
|
386
|
+
*
|
|
387
|
+
* The file's binary must be loaded. Call file.attachBinary() first if needed.
|
|
388
|
+
* Persist with file.toJSON() after calling this method.
|
|
389
|
+
*
|
|
390
|
+
* @returns The new MajikSignature.
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* file.attachBinary(await r2.get(row.r2_key).arrayBuffer());
|
|
394
|
+
* const sig = await majik.resignMajikFile(file);
|
|
395
|
+
* await supabase
|
|
396
|
+
* .from("majik_files")
|
|
397
|
+
* .update({ signature: file.signatureRaw, last_update: file.lastUpdate })
|
|
398
|
+
* .eq("id", file.id);
|
|
399
|
+
*/
|
|
400
|
+
resignMajikFile(file: MajikFile, options?: {
|
|
401
|
+
accountId?: string;
|
|
402
|
+
contentType?: string;
|
|
403
|
+
timestamp?: string;
|
|
404
|
+
}): Promise<MajikSignature>;
|
|
237
405
|
listCachedEnvelopes(offset?: number, limit?: number): Promise<EnvelopeCacheItem[]>;
|
|
238
406
|
clearCachedEnvelopes(): Promise<boolean>;
|
|
239
407
|
/**
|
|
@@ -252,6 +420,162 @@ export declare class MajikMessage {
|
|
|
252
420
|
off(event: MajikMessageEvents, callback?: EventCallback): void;
|
|
253
421
|
private emit;
|
|
254
422
|
private handleEnvelope;
|
|
423
|
+
/**
|
|
424
|
+
* Sign raw bytes or a string using the active account.
|
|
425
|
+
*
|
|
426
|
+
* The active account is unlocked automatically if needed.
|
|
427
|
+
* This is the MajikMessage equivalent of MajikSignature.sign() — it resolves
|
|
428
|
+
* the signing key from the keystore so you don't have to manage it yourself.
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* const sig = await majik.signContent(documentBytes, { contentType: "application/pdf" });
|
|
432
|
+
* const b64 = sig.serialize(); // store alongside the document
|
|
433
|
+
*/
|
|
434
|
+
signContent(content: Uint8Array | string, options?: {
|
|
435
|
+
contentType?: string;
|
|
436
|
+
timestamp?: string;
|
|
437
|
+
accountId?: string;
|
|
438
|
+
}): Promise<MajikSignature>;
|
|
439
|
+
/**
|
|
440
|
+
* Sign a file and embed the signature directly into it using the active account.
|
|
441
|
+
*
|
|
442
|
+
* Format is auto-detected from magic bytes — PDF stays PDF, WAV stays WAV, etc.
|
|
443
|
+
* Strips any existing signature before signing (idempotent re-signing).
|
|
444
|
+
* The active account is unlocked automatically if needed.
|
|
445
|
+
*
|
|
446
|
+
* @example
|
|
447
|
+
* const { blob: signedPdf } = await majik.signFile(pdfBlob);
|
|
448
|
+
* // signedPdf is a valid PDF with the signature embedded in its metadata
|
|
449
|
+
*
|
|
450
|
+
* @example — non-active account
|
|
451
|
+
* const { blob } = await majik.signFile(wavBlob, { accountId: "acc_xyz" });
|
|
452
|
+
*/
|
|
453
|
+
signFile(file: Blob, options?: {
|
|
454
|
+
contentType?: string;
|
|
455
|
+
timestamp?: string;
|
|
456
|
+
mimeType?: string;
|
|
457
|
+
accountId?: string;
|
|
458
|
+
}): Promise<{
|
|
459
|
+
blob: Blob;
|
|
460
|
+
signature: MajikSignature;
|
|
461
|
+
handler: string;
|
|
462
|
+
mimeType: string;
|
|
463
|
+
}>;
|
|
464
|
+
/**
|
|
465
|
+
* Verify raw bytes or a string against a MajikSignature.
|
|
466
|
+
*
|
|
467
|
+
* The signer can be identified by:
|
|
468
|
+
* - A contact ID from the contact directory
|
|
469
|
+
* - A raw base64 public key string (same format used in contacts)
|
|
470
|
+
* - A MajikKey instance directly
|
|
471
|
+
*
|
|
472
|
+
* If no signer is provided, the public keys embedded in the signature
|
|
473
|
+
* envelope are used (self-reported — see security note below).
|
|
474
|
+
*
|
|
475
|
+
* > ⚠️ When no signer is provided, the extracted public keys are self-reported
|
|
476
|
+
* > by whoever created the signature. Always cross-check `result.signerId`
|
|
477
|
+
* > against a known contact fingerprint before trusting the result.
|
|
478
|
+
*
|
|
479
|
+
* @example — verify against a known contact
|
|
480
|
+
* const result = await majik.verifyContent(docBytes, sig, { contactId: "contact_abc" });
|
|
481
|
+
* if (result.valid) console.log("Authentic, signed by:", result.signerId);
|
|
482
|
+
*
|
|
483
|
+
* @example — verify using embedded keys (self-reported)
|
|
484
|
+
* const result = await majik.verifyContent(docBytes, sig);
|
|
485
|
+
* // always check result.signerId matches a known fingerprint
|
|
486
|
+
*/
|
|
487
|
+
verifyContent(content: Uint8Array | string, signature: MajikSignature | MajikSignatureJSON, options?: {
|
|
488
|
+
contactId?: string;
|
|
489
|
+
publicKeyBase64?: string;
|
|
490
|
+
key?: MajikKey;
|
|
491
|
+
expectedSignerId?: string;
|
|
492
|
+
}): Promise<VerificationResult>;
|
|
493
|
+
/**
|
|
494
|
+
* Verify a file's embedded signature.
|
|
495
|
+
*
|
|
496
|
+
* The signer can be identified by:
|
|
497
|
+
* - A contact ID from the contact directory
|
|
498
|
+
* - A raw base64 public key string
|
|
499
|
+
* - A MajikKey instance directly
|
|
500
|
+
*
|
|
501
|
+
* If no signer is provided, the public keys embedded in the signature
|
|
502
|
+
* envelope are used (self-reported — see security note on verifyContent).
|
|
503
|
+
*
|
|
504
|
+
* @example — verify a signed PDF against a known contact
|
|
505
|
+
* const result = await majik.verifyFile(signedPdf, { contactId: "contact_abc" });
|
|
506
|
+
* if (result.valid) console.log("Verified:", result.signerId, result.timestamp);
|
|
507
|
+
*
|
|
508
|
+
* @example — check own signed file using active account
|
|
509
|
+
* const result = await majik.verifyFile(signedWav, {
|
|
510
|
+
* contactId: majik.getActiveAccount()?.id,
|
|
511
|
+
* });
|
|
512
|
+
*/
|
|
513
|
+
verifyFile(file: Blob, options?: {
|
|
514
|
+
contactId?: string;
|
|
515
|
+
publicKeyBase64?: string;
|
|
516
|
+
key?: MajikKey;
|
|
517
|
+
expectedSignerId?: string;
|
|
518
|
+
mimeType?: string;
|
|
519
|
+
}): Promise<VerificationResult & {
|
|
520
|
+
handler?: string;
|
|
521
|
+
reason?: string;
|
|
522
|
+
}>;
|
|
523
|
+
/**
|
|
524
|
+
* Extract the embedded MajikSignature from a file.
|
|
525
|
+
* Returns a fully typed MajikSignature instance, or null if not found.
|
|
526
|
+
*
|
|
527
|
+
* Does not verify — use verifyFile() to verify.
|
|
528
|
+
*
|
|
529
|
+
* @example
|
|
530
|
+
* const sig = await majik.extractSignature(file);
|
|
531
|
+
* if (sig) console.log("Signed by:", sig.signerId, "at", sig.timestamp);
|
|
532
|
+
*/
|
|
533
|
+
extractSignature(file: Blob, options?: {
|
|
534
|
+
mimeType?: string;
|
|
535
|
+
}): Promise<MajikSignature | null>;
|
|
536
|
+
/**
|
|
537
|
+
* Return a clean copy of the file with any embedded signature removed.
|
|
538
|
+
* The returned bytes are exactly what was originally signed.
|
|
539
|
+
*
|
|
540
|
+
* Useful before re-processing or re-encrypting a signed file.
|
|
541
|
+
*
|
|
542
|
+
* @example
|
|
543
|
+
* const originalBlob = await majik.stripSignature(signedMp4);
|
|
544
|
+
*/
|
|
545
|
+
stripSignature(file: Blob, options?: {
|
|
546
|
+
mimeType?: string;
|
|
547
|
+
}): Promise<Blob>;
|
|
548
|
+
/**
|
|
549
|
+
* Check whether a file contains an embedded MajikSignature.
|
|
550
|
+
* Does not verify — purely a structural presence check.
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* if (await majik.isFileSigned(file)) {
|
|
554
|
+
* const result = await majik.verifyFile(file, { contactId });
|
|
555
|
+
* }
|
|
556
|
+
*/
|
|
557
|
+
isFileSigned(file: Blob, options?: {
|
|
558
|
+
mimeType?: string;
|
|
559
|
+
}): Promise<boolean>;
|
|
560
|
+
/**
|
|
561
|
+
* Get the public keys for the active account, ready for use with
|
|
562
|
+
* MajikSignature.verify() or for sharing with another party.
|
|
563
|
+
*
|
|
564
|
+
* Works on locked keys — only reads public fields.
|
|
565
|
+
*
|
|
566
|
+
* @example
|
|
567
|
+
* const myKeys = await majik.getSigningPublicKeys();
|
|
568
|
+
* // share myKeys with someone so they can verify your signatures
|
|
569
|
+
*/
|
|
570
|
+
getSigningPublicKeys(accountId?: string): Promise<MajikSignerPublicKeys>;
|
|
571
|
+
/**
|
|
572
|
+
* Resolve MajikSignerPublicKeys from whichever signer hint was provided.
|
|
573
|
+
* Returns null if no hint was given (caller should fall back to self-reported keys).
|
|
574
|
+
*
|
|
575
|
+
* Mirrors the _resolveRecipients / _resolveFileIdentity pattern used
|
|
576
|
+
* throughout MajikMessage — consistent account/contact resolution in one place.
|
|
577
|
+
*/
|
|
578
|
+
private _resolveSignerPublicKeys;
|
|
255
579
|
setPIN(pin: string): Promise<void>;
|
|
256
580
|
clearPIN(): Promise<void>;
|
|
257
581
|
isValidPIN(pin: string): Promise<boolean>;
|