@majikah/majik-message 0.2.3 → 0.2.5

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.
@@ -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";
@@ -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
  }
@@ -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
- * @throws Error if no active account, account has no ML-KEM keys, or a
166
- * recipient cannot be resolved from the contact directory.
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
- * @example self-encrypted user upload
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
- * Otherwise the active account is tried first.
201
- * 2. For group files (multiple recipients), if the first account fails,
202
- * every own account is tried in sequence until one succeeds.
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
- * @throws Error if no own account can decrypt the file.
214
- * @throws MajikFileError (re-thrown) on corrupt binary, wrong key, or format
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
- * @example basic usage
218
- * ```ts
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 — explicit account (e.g. non-active account in a multi-account UI)
207
+ * @example — basic usage with metadata row from Supabase
225
208
  * ```ts
226
- * const rawBytes = await majik.decryptFile({
227
- * source: mjkbBytes,
228
- * accountId: "acc_xyz",
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>;