@majikah/majik-message 0.2.5 → 0.2.8

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.
@@ -208,6 +208,7 @@ export declare class MajikKeyStore {
208
208
  * Drop-in for MajikKeyStore.deleteIdentity().
209
209
  */
210
210
  static deleteIdentity(id: string): Promise<void>;
211
+ static deleteAll(): Promise<void>;
211
212
  /**
212
213
  * Drop-in for MajikKeyStore.generateMnemonic().
213
214
  */
@@ -519,6 +519,20 @@ export class MajikKeyStore {
519
519
  static async deleteIdentity(id) {
520
520
  return this.delete(id);
521
521
  }
522
+ static async deleteAll() {
523
+ const db = await this._getDB();
524
+ const clearStore = (storeName) => new Promise((resolve, reject) => {
525
+ if (!db.objectStoreNames.contains(storeName))
526
+ return resolve();
527
+ const tx = db.transaction(storeName, "readwrite");
528
+ const req = tx.objectStore(storeName).clear();
529
+ req.onsuccess = () => resolve();
530
+ req.onerror = () => reject(new MajikKeyStoreError(`Failed to clear store: ${storeName}`, req.error));
531
+ });
532
+ await clearStore(STORE_NAME);
533
+ await clearStore(LEGACY_STORE_NAME);
534
+ this._keys.clear();
535
+ }
522
536
  /**
523
537
  * Drop-in for MajikKeyStore.generateMnemonic().
524
538
  */
@@ -402,6 +402,151 @@ export declare class MajikMessage {
402
402
  contentType?: string;
403
403
  timestamp?: string;
404
404
  }): Promise<MajikSignature>;
405
+ /**
406
+ * Convenience alias for signing a plain string.
407
+ *
408
+ * Identical to signContent() but accepts only strings — makes call-sites
409
+ * that deal exclusively with text cleaner (no Uint8Array overload noise).
410
+ *
411
+ * @example
412
+ * const sig = await majik.signText("Hello world", { contentType: "text/plain" });
413
+ * const b64 = sig.serialize(); // store alongside the text
414
+ */
415
+ signText(text: string, options?: {
416
+ contentType?: string;
417
+ timestamp?: string;
418
+ accountId?: string;
419
+ }): Promise<MajikSignature>;
420
+ /**
421
+ * Sign content and return both the MajikSignature instance and a portable
422
+ * base64-serialized string in one call.
423
+ *
424
+ * The serialized string is safe to store in a database column, embed in a
425
+ * JSON field, pass in an HTTP header, or encode in a QR code alongside the
426
+ * original content. Pass it back to verifyDetached() to verify.
427
+ *
428
+ * @example — sign a document and store the detached signature
429
+ * const { serialized } = await majik.signAndDetach(docBytes, {
430
+ * contentType: "application/pdf",
431
+ * });
432
+ * await db.insert({ doc_id, signature: serialized });
433
+ *
434
+ * @example — sign a text message
435
+ * const { signature, serialized } = await majik.signAndDetach("Hello!", {
436
+ * contentType: "text/plain",
437
+ * });
438
+ */
439
+ signAndDetach(content: Uint8Array | string, options?: {
440
+ contentType?: string;
441
+ timestamp?: string;
442
+ accountId?: string;
443
+ }): Promise<{
444
+ signature: MajikSignature;
445
+ serialized: string;
446
+ }>;
447
+ /**
448
+ * Verify a plain string against a MajikSignature.
449
+ *
450
+ * Accepts the signature as a MajikSignature instance, a MajikSignatureJSON
451
+ * object, or a base64-serialized string — whichever form is easiest at the
452
+ * call-site.
453
+ *
454
+ * The signer can be identified by contact ID, raw public key base64, or a
455
+ * MajikKey instance. If none is provided the public keys embedded in the
456
+ * signature envelope are used (self-reported — cross-check result.signerId
457
+ * against a known contact fingerprint before trusting).
458
+ *
459
+ * @example
460
+ * const result = await majik.verifyText("Hello world", sig, {
461
+ * contactId: "contact_abc",
462
+ * });
463
+ * if (result.valid) console.log("Authentic");
464
+ */
465
+ verifyText(text: string, signature: MajikSignature | MajikSignatureJSON | string, options?: {
466
+ contactId?: string;
467
+ publicKeyBase64?: string;
468
+ key?: MajikKey;
469
+ expectedSignerId?: string;
470
+ }): Promise<VerificationResult>;
471
+ /**
472
+ * Verify content against a base64-serialized detached signature string.
473
+ *
474
+ * This is the pair to signAndDetach() — designed for call-sites that retrieve
475
+ * a stored base64 signature from a database or API and want to verify without
476
+ * importing MajikSignature themselves.
477
+ *
478
+ * The signer can be identified by contact ID, raw public key base64, or a
479
+ * MajikKey. If none is provided, self-reported keys from the envelope are used
480
+ * (see security note on verifyContent).
481
+ *
482
+ * @example
483
+ * const row = await db.findOne({ doc_id });
484
+ * const result = await majik.verifyDetached(docBytes, row.signature, {
485
+ * contactId: row.signer_contact_id,
486
+ * });
487
+ * if (result.valid) console.log("Signed by", result.signerId);
488
+ */
489
+ verifyDetached(content: Uint8Array | string, serializedSignature: string, options?: {
490
+ contactId?: string;
491
+ publicKeyBase64?: string;
492
+ key?: MajikKey;
493
+ expectedSignerId?: string;
494
+ }): Promise<VerificationResult>;
495
+ /**
496
+ * Deserialize a base64 signature string into a MajikSignature instance.
497
+ *
498
+ * Round-trip partner for MajikSignature.serialize() / sig.toString().
499
+ * Use when you have a stored base64 string and need to inspect or pass
500
+ * the instance to another method.
501
+ *
502
+ * Throws MajikSignatureSerializationError on malformed input.
503
+ *
504
+ * @example
505
+ * const sig = majik.deserializeSignature(storedBase64);
506
+ * console.log(sig.signerId, sig.timestamp);
507
+ */
508
+ deserializeSignature(serialized: string): MajikSignature;
509
+ /**
510
+ * Extract lightweight metadata from a base64 or JSON signature string
511
+ * without performing cryptographic verification.
512
+ *
513
+ * Useful for displaying "Signed by X at Y" in a UI before the user
514
+ * explicitly triggers a verification step.
515
+ *
516
+ * Returns null if the string cannot be parsed as a MajikSignature.
517
+ *
518
+ * @example
519
+ * const meta = majik.getSignatureMetadata(storedSig);
520
+ * if (meta) {
521
+ * const contact = majik.getContactByID(meta.signerId);
522
+ * console.log(`Signed by ${contact?.meta?.label ?? meta.signerId} at ${meta.timestamp}`);
523
+ * }
524
+ */
525
+ getSignatureMetadata(serialized: string): {
526
+ signerId: string;
527
+ timestamp: string;
528
+ contentType: string | undefined;
529
+ contentHash: string;
530
+ version: number;
531
+ } | null;
532
+ /**
533
+ * Check whether an account has signing keys without throwing.
534
+ *
535
+ * Use this as a fast boolean guard before showing signing UI or before
536
+ * calling any sign* method — those methods throw if signing keys are absent,
537
+ * so checking first lets you degrade gracefully (e.g. hide a "Sign" button).
538
+ *
539
+ * Checks the in-memory keystore cache only — the account must be loaded.
540
+ * Returns false for unknown accounts rather than throwing.
541
+ *
542
+ * @example
543
+ * if (!majik.hasSigningCapability()) {
544
+ * showUpgradePrompt("Re-import your account to enable signing");
545
+ * return;
546
+ * }
547
+ * const sig = await majik.signText(message);
548
+ */
549
+ hasSigningCapability(accountId?: string): boolean;
405
550
  listCachedEnvelopes(offset?: number, limit?: number): Promise<EnvelopeCacheItem[]>;
406
551
  clearCachedEnvelopes(): Promise<boolean>;
407
552
  /**
@@ -461,6 +606,42 @@ export declare class MajikMessage {
461
606
  handler: string;
462
607
  mimeType: string;
463
608
  }>;
609
+ /**
610
+ * Sign multiple file blobs with the active (or specified) account in one call.
611
+ *
612
+ * Each file is signed independently — a failure on one does not abort the
613
+ * others. Check result.error on each item to handle partial failures.
614
+ *
615
+ * The hasSigningKeys check is done once upfront before any signing begins,
616
+ * so the whole batch fails fast if the account can't sign rather than
617
+ * discovering it mid-batch.
618
+ *
619
+ * @example
620
+ * const results = await majik.batchSignFiles([
621
+ * { file: pdfBlob, contentType: "application/pdf" },
622
+ * { file: wavBlob, contentType: "audio/wav" },
623
+ * { file: mp4Blob, contentType: "video/mp4" },
624
+ * ]);
625
+ * for (const r of results) {
626
+ * if (r.error) console.error("Failed:", r.error.message);
627
+ * else await r2.put(key, await r.blob!.arrayBuffer());
628
+ * }
629
+ */
630
+ batchSignFiles(files: Array<{
631
+ file: Blob;
632
+ contentType?: string;
633
+ timestamp?: string;
634
+ mimeType?: string;
635
+ }>, options?: {
636
+ accountId?: string;
637
+ }): Promise<Array<{
638
+ blob: Blob | null;
639
+ signature: MajikSignature | null;
640
+ serialized: string | null;
641
+ handler: string | null;
642
+ mimeType: string | null;
643
+ error: Error | null;
644
+ }>>;
464
645
  /**
465
646
  * Verify raw bytes or a string against a MajikSignature.
466
647
  *
@@ -520,6 +701,34 @@ export declare class MajikMessage {
520
701
  handler?: string;
521
702
  reason?: string;
522
703
  }>;
704
+ /**
705
+ * Verify multiple files' embedded signatures against the same signer in
706
+ * one call.
707
+ *
708
+ * Each file is verified independently — a failed verification sets
709
+ * result.valid = false and populates result.error, it does not throw.
710
+ *
711
+ * @example
712
+ * const results = await majik.batchVerifyFiles(
713
+ * [pdfBlob, wavBlob, mp4Blob],
714
+ * { contactId: "contact_abc" },
715
+ * );
716
+ * const allValid = results.every(r => r.valid);
717
+ */
718
+ batchVerifyFiles(files: Array<Blob | {
719
+ file: Blob;
720
+ mimeType?: string;
721
+ expectedSignerId?: string;
722
+ }>, options?: {
723
+ contactId?: string;
724
+ publicKeyBase64?: string;
725
+ key?: MajikKey;
726
+ expectedSignerId?: string;
727
+ }): Promise<Array<VerificationResult & {
728
+ handler: string | null;
729
+ mimeType: string | null;
730
+ error: Error | null;
731
+ }>>;
523
732
  /**
524
733
  * Extract the embedded MajikSignature from a file.
525
734
  * Returns a fully typed MajikSignature instance, or null if not found.
@@ -568,6 +777,52 @@ export declare class MajikMessage {
568
777
  * // share myKeys with someone so they can verify your signatures
569
778
  */
570
779
  getSigningPublicKeys(accountId?: string): Promise<MajikSignerPublicKeys>;
780
+ /**
781
+ * Re-sign a file blob — strips any existing embedded signature, signs
782
+ * with the active (or specified) account, and returns the newly signed blob.
783
+ *
784
+ * Use after key rotation or when the signing account changes. The returned
785
+ * blob is the same format as the input — PDF stays PDF, WAV stays WAV.
786
+ *
787
+ * Distinct from resignMajikFile() which operates on a MajikFile instance
788
+ * (the encrypted .mjkb container). This operates on a plain file Blob.
789
+ *
790
+ * @example
791
+ * const { blob } = await majik.resignFile(oldSignedPdf);
792
+ * await r2.put(key, await blob.arrayBuffer());
793
+ */
794
+ resignFile(file: Blob, options?: {
795
+ contentType?: string;
796
+ timestamp?: string;
797
+ mimeType?: string;
798
+ accountId?: string;
799
+ }): Promise<{
800
+ blob: Blob;
801
+ signature: MajikSignature;
802
+ handler: string;
803
+ mimeType: string;
804
+ }>;
805
+ /**
806
+ * Extract metadata from a file's embedded signature without verifying it.
807
+ *
808
+ * Useful for rendering "Signed by X at Y" in a UI before the user
809
+ * explicitly triggers a verify step, or for routing to the correct
810
+ * contact record before calling verifyFile().
811
+ *
812
+ * Returns null if the file has no embedded signature or the JSON is
813
+ * structurally malformed.
814
+ *
815
+ * @example
816
+ * const info = await majik.getFileSignatureInfo(pdfBlob);
817
+ * if (info) {
818
+ * const contact = majik.getContactByID(info.signerId);
819
+ * console.log(`Signed by ${contact?.meta?.label ?? info.signerId}`);
820
+ * console.log(`Format handled by: ${info.handler}`);
821
+ * }
822
+ */
823
+ getFileSignatureInfo(file: Blob, options?: {
824
+ mimeType?: string;
825
+ }): Promise<MajikSignature | null>;
571
826
  /**
572
827
  * Resolve MajikSignerPublicKeys from whichever signer hint was provided.
573
828
  * Returns null if no hint was given (caller should fall back to self-reported keys).
@@ -1038,6 +1038,194 @@ export class MajikMessage {
1038
1038
  file.removeSignature();
1039
1039
  return this.signMajikFile(file, options);
1040
1040
  }
1041
+ // ── Text / Detached Signing ───────────────────────────────────────────────────
1042
+ /**
1043
+ * Convenience alias for signing a plain string.
1044
+ *
1045
+ * Identical to signContent() but accepts only strings — makes call-sites
1046
+ * that deal exclusively with text cleaner (no Uint8Array overload noise).
1047
+ *
1048
+ * @example
1049
+ * const sig = await majik.signText("Hello world", { contentType: "text/plain" });
1050
+ * const b64 = sig.serialize(); // store alongside the text
1051
+ */
1052
+ async signText(text, options) {
1053
+ if (!text?.trim())
1054
+ throw new Error("signText: text must be a non-empty string");
1055
+ return this.signContent(text, options);
1056
+ }
1057
+ /**
1058
+ * Sign content and return both the MajikSignature instance and a portable
1059
+ * base64-serialized string in one call.
1060
+ *
1061
+ * The serialized string is safe to store in a database column, embed in a
1062
+ * JSON field, pass in an HTTP header, or encode in a QR code alongside the
1063
+ * original content. Pass it back to verifyDetached() to verify.
1064
+ *
1065
+ * @example — sign a document and store the detached signature
1066
+ * const { serialized } = await majik.signAndDetach(docBytes, {
1067
+ * contentType: "application/pdf",
1068
+ * });
1069
+ * await db.insert({ doc_id, signature: serialized });
1070
+ *
1071
+ * @example — sign a text message
1072
+ * const { signature, serialized } = await majik.signAndDetach("Hello!", {
1073
+ * contentType: "text/plain",
1074
+ * });
1075
+ */
1076
+ async signAndDetach(content, options) {
1077
+ const signature = await this.signContent(content, options);
1078
+ return { signature, serialized: signature.serialize() };
1079
+ }
1080
+ // ── Text / Detached Verification ──────────────────────────────────────────────
1081
+ /**
1082
+ * Verify a plain string against a MajikSignature.
1083
+ *
1084
+ * Accepts the signature as a MajikSignature instance, a MajikSignatureJSON
1085
+ * object, or a base64-serialized string — whichever form is easiest at the
1086
+ * call-site.
1087
+ *
1088
+ * The signer can be identified by contact ID, raw public key base64, or a
1089
+ * MajikKey instance. If none is provided the public keys embedded in the
1090
+ * signature envelope are used (self-reported — cross-check result.signerId
1091
+ * against a known contact fingerprint before trusting).
1092
+ *
1093
+ * @example
1094
+ * const result = await majik.verifyText("Hello world", sig, {
1095
+ * contactId: "contact_abc",
1096
+ * });
1097
+ * if (result.valid) console.log("Authentic");
1098
+ */
1099
+ async verifyText(text, signature, options) {
1100
+ if (!text?.trim())
1101
+ throw new Error("verifyText: text must be a non-empty string");
1102
+ const sig = typeof signature === "string"
1103
+ ? MajikSignature.deserialize(signature)
1104
+ : signature;
1105
+ return this.verifyContent(text, sig, options);
1106
+ }
1107
+ /**
1108
+ * Verify content against a base64-serialized detached signature string.
1109
+ *
1110
+ * This is the pair to signAndDetach() — designed for call-sites that retrieve
1111
+ * a stored base64 signature from a database or API and want to verify without
1112
+ * importing MajikSignature themselves.
1113
+ *
1114
+ * The signer can be identified by contact ID, raw public key base64, or a
1115
+ * MajikKey. If none is provided, self-reported keys from the envelope are used
1116
+ * (see security note on verifyContent).
1117
+ *
1118
+ * @example
1119
+ * const row = await db.findOne({ doc_id });
1120
+ * const result = await majik.verifyDetached(docBytes, row.signature, {
1121
+ * contactId: row.signer_contact_id,
1122
+ * });
1123
+ * if (result.valid) console.log("Signed by", result.signerId);
1124
+ */
1125
+ async verifyDetached(content, serializedSignature, options) {
1126
+ if (!serializedSignature?.trim()) {
1127
+ throw new Error("verifyDetached: serializedSignature must be a non-empty string");
1128
+ }
1129
+ let sig;
1130
+ try {
1131
+ sig = MajikSignature.deserialize(serializedSignature);
1132
+ }
1133
+ catch {
1134
+ // Fallback: maybe caller passed raw JSON rather than base64
1135
+ try {
1136
+ sig = MajikSignature.fromJSON(serializedSignature);
1137
+ }
1138
+ catch {
1139
+ throw new Error("verifyDetached: could not parse signature — expected a base64 " +
1140
+ "string from sig.serialize() or a JSON string from sig.toJSON()");
1141
+ }
1142
+ }
1143
+ return this.verifyContent(content, sig, options);
1144
+ }
1145
+ // ── Signature Serialization Helpers ──────────────────────────────────────────
1146
+ /**
1147
+ * Deserialize a base64 signature string into a MajikSignature instance.
1148
+ *
1149
+ * Round-trip partner for MajikSignature.serialize() / sig.toString().
1150
+ * Use when you have a stored base64 string and need to inspect or pass
1151
+ * the instance to another method.
1152
+ *
1153
+ * Throws MajikSignatureSerializationError on malformed input.
1154
+ *
1155
+ * @example
1156
+ * const sig = majik.deserializeSignature(storedBase64);
1157
+ * console.log(sig.signerId, sig.timestamp);
1158
+ */
1159
+ deserializeSignature(serialized) {
1160
+ if (!serialized?.trim()) {
1161
+ throw new Error("deserializeSignature: input must be a non-empty string");
1162
+ }
1163
+ return MajikSignature.deserialize(serialized);
1164
+ }
1165
+ /**
1166
+ * Extract lightweight metadata from a base64 or JSON signature string
1167
+ * without performing cryptographic verification.
1168
+ *
1169
+ * Useful for displaying "Signed by X at Y" in a UI before the user
1170
+ * explicitly triggers a verification step.
1171
+ *
1172
+ * Returns null if the string cannot be parsed as a MajikSignature.
1173
+ *
1174
+ * @example
1175
+ * const meta = majik.getSignatureMetadata(storedSig);
1176
+ * if (meta) {
1177
+ * const contact = majik.getContactByID(meta.signerId);
1178
+ * console.log(`Signed by ${contact?.meta?.label ?? meta.signerId} at ${meta.timestamp}`);
1179
+ * }
1180
+ */
1181
+ getSignatureMetadata(serialized) {
1182
+ if (!serialized?.trim())
1183
+ return null;
1184
+ try {
1185
+ let sig;
1186
+ try {
1187
+ sig = MajikSignature.deserialize(serialized);
1188
+ }
1189
+ catch {
1190
+ sig = MajikSignature.fromJSON(serialized);
1191
+ }
1192
+ return {
1193
+ signerId: sig.signerId,
1194
+ timestamp: sig.timestamp,
1195
+ contentType: sig.contentType,
1196
+ contentHash: sig.contentHash,
1197
+ version: sig.version,
1198
+ };
1199
+ }
1200
+ catch {
1201
+ return null;
1202
+ }
1203
+ }
1204
+ // ── Signing Capability Guard ──────────────────────────────────────────────────
1205
+ /**
1206
+ * Check whether an account has signing keys without throwing.
1207
+ *
1208
+ * Use this as a fast boolean guard before showing signing UI or before
1209
+ * calling any sign* method — those methods throw if signing keys are absent,
1210
+ * so checking first lets you degrade gracefully (e.g. hide a "Sign" button).
1211
+ *
1212
+ * Checks the in-memory keystore cache only — the account must be loaded.
1213
+ * Returns false for unknown accounts rather than throwing.
1214
+ *
1215
+ * @example
1216
+ * if (!majik.hasSigningCapability()) {
1217
+ * showUpgradePrompt("Re-import your account to enable signing");
1218
+ * return;
1219
+ * }
1220
+ * const sig = await majik.signText(message);
1221
+ */
1222
+ hasSigningCapability(accountId) {
1223
+ const id = accountId ?? this.getActiveAccount()?.id;
1224
+ if (!id)
1225
+ return false;
1226
+ const key = MajikKeyStore.get(id);
1227
+ return key?.hasSigningKeys === true;
1228
+ }
1041
1229
  // ── Envelope Cache ────────────────────────────────────────────────────────
1042
1230
  async listCachedEnvelopes(offset = 0, limit = 50) {
1043
1231
  return this.envelopeCache.listRecent(offset, limit);
@@ -1215,6 +1403,68 @@ export class MajikMessage {
1215
1403
  throw err;
1216
1404
  }
1217
1405
  }
1406
+ /**
1407
+ * Sign multiple file blobs with the active (or specified) account in one call.
1408
+ *
1409
+ * Each file is signed independently — a failure on one does not abort the
1410
+ * others. Check result.error on each item to handle partial failures.
1411
+ *
1412
+ * The hasSigningKeys check is done once upfront before any signing begins,
1413
+ * so the whole batch fails fast if the account can't sign rather than
1414
+ * discovering it mid-batch.
1415
+ *
1416
+ * @example
1417
+ * const results = await majik.batchSignFiles([
1418
+ * { file: pdfBlob, contentType: "application/pdf" },
1419
+ * { file: wavBlob, contentType: "audio/wav" },
1420
+ * { file: mp4Blob, contentType: "video/mp4" },
1421
+ * ]);
1422
+ * for (const r of results) {
1423
+ * if (r.error) console.error("Failed:", r.error.message);
1424
+ * else await r2.put(key, await r.blob!.arrayBuffer());
1425
+ * }
1426
+ */
1427
+ async batchSignFiles(files, options) {
1428
+ const id = options?.accountId ?? this.getActiveAccount()?.id;
1429
+ if (!id)
1430
+ throw new Error("No active account — call setActiveAccount() first");
1431
+ await MajikKeyStore.ensureUnlocked(id);
1432
+ const key = MajikKeyStore.get(id);
1433
+ if (!key)
1434
+ throw new Error(`Account not found in keystore: "${id}"`);
1435
+ if (!key.hasSigningKeys) {
1436
+ throw new Error(`Account "${id}" has no signing keys. ` +
1437
+ `Re-import via importAccountFromMnemonicBackup() to enable signing.`);
1438
+ }
1439
+ return Promise.all(files.map(async ({ file, contentType, timestamp, mimeType }) => {
1440
+ try {
1441
+ const result = await MajikSignature.signFile(file, key, {
1442
+ contentType,
1443
+ timestamp,
1444
+ mimeType,
1445
+ });
1446
+ return {
1447
+ blob: result.blob,
1448
+ signature: result.signature,
1449
+ serialized: result.signature.serialize(),
1450
+ handler: result.handler,
1451
+ mimeType: result.mimeType,
1452
+ error: null,
1453
+ };
1454
+ }
1455
+ catch (err) {
1456
+ this.emit("error", err, { context: "batchSignFiles" });
1457
+ return {
1458
+ blob: null,
1459
+ signature: null,
1460
+ serialized: null,
1461
+ handler: null,
1462
+ mimeType: null,
1463
+ error: err instanceof Error ? err : new Error(String(err)),
1464
+ };
1465
+ }
1466
+ }));
1467
+ }
1218
1468
  // ── Verification ──────────────────────────────────────────────────────────
1219
1469
  /**
1220
1470
  * Verify raw bytes or a string against a MajikSignature.
@@ -1308,6 +1558,82 @@ export class MajikMessage {
1308
1558
  throw err;
1309
1559
  }
1310
1560
  }
1561
+ /**
1562
+ * Verify multiple files' embedded signatures against the same signer in
1563
+ * one call.
1564
+ *
1565
+ * Each file is verified independently — a failed verification sets
1566
+ * result.valid = false and populates result.error, it does not throw.
1567
+ *
1568
+ * @example
1569
+ * const results = await majik.batchVerifyFiles(
1570
+ * [pdfBlob, wavBlob, mp4Blob],
1571
+ * { contactId: "contact_abc" },
1572
+ * );
1573
+ * const allValid = results.every(r => r.valid);
1574
+ */
1575
+ async batchVerifyFiles(files, options) {
1576
+ // Resolve public keys once — reused across all files in the batch
1577
+ const publicKeys = await this._resolveSignerPublicKeys(options).catch(() => null);
1578
+ return Promise.all(files.map(async (entry) => {
1579
+ const { file, mimeType, expectedSignerId } = entry instanceof Blob
1580
+ ? {
1581
+ file: entry,
1582
+ mimeType: undefined,
1583
+ expectedSignerId: options?.expectedSignerId,
1584
+ }
1585
+ : {
1586
+ ...entry,
1587
+ expectedSignerId: entry.expectedSignerId ?? options?.expectedSignerId,
1588
+ };
1589
+ try {
1590
+ let result;
1591
+ if (publicKeys) {
1592
+ result = await MajikSignature.verifyFile(file, publicKeys, {
1593
+ mimeType,
1594
+ expectedSignerId,
1595
+ });
1596
+ }
1597
+ else {
1598
+ // No signer hint — use self-reported keys from each file's envelope
1599
+ const extracted = await MajikSignature.extractFrom(file, {
1600
+ mimeType,
1601
+ });
1602
+ if (!extracted) {
1603
+ return {
1604
+ valid: false,
1605
+ signerId: "",
1606
+ contentHash: "",
1607
+ timestamp: new Date().toISOString(),
1608
+ reason: "No embedded signature found",
1609
+ handler: null,
1610
+ mimeType: mimeType ?? null,
1611
+ error: null,
1612
+ };
1613
+ }
1614
+ result = await MajikSignature.verifyFile(file, extracted.extractPublicKeys(), { mimeType, expectedSignerId });
1615
+ }
1616
+ return {
1617
+ ...result,
1618
+ handler: result.handler ?? null,
1619
+ mimeType: mimeType ?? null,
1620
+ error: null,
1621
+ };
1622
+ }
1623
+ catch (err) {
1624
+ this.emit("error", err, { context: "batchVerifyFiles" });
1625
+ return {
1626
+ valid: false,
1627
+ signerId: "",
1628
+ contentHash: "",
1629
+ timestamp: new Date().toISOString(),
1630
+ handler: null,
1631
+ mimeType: mimeType ?? null,
1632
+ error: err instanceof Error ? err : new Error(String(err)),
1633
+ };
1634
+ }
1635
+ }));
1636
+ }
1311
1637
  // ── Signature Utilities ───────────────────────────────────────────────────
1312
1638
  /**
1313
1639
  * Extract the embedded MajikSignature from a file.
@@ -1387,6 +1713,52 @@ export class MajikMessage {
1387
1713
  }
1388
1714
  return MajikSignature.publicKeysFromMajikKey(key);
1389
1715
  }
1716
+ /**
1717
+ * Re-sign a file blob — strips any existing embedded signature, signs
1718
+ * with the active (or specified) account, and returns the newly signed blob.
1719
+ *
1720
+ * Use after key rotation or when the signing account changes. The returned
1721
+ * blob is the same format as the input — PDF stays PDF, WAV stays WAV.
1722
+ *
1723
+ * Distinct from resignMajikFile() which operates on a MajikFile instance
1724
+ * (the encrypted .mjkb container). This operates on a plain file Blob.
1725
+ *
1726
+ * @example
1727
+ * const { blob } = await majik.resignFile(oldSignedPdf);
1728
+ * await r2.put(key, await blob.arrayBuffer());
1729
+ */
1730
+ async resignFile(file, options) {
1731
+ // signFile already strips before signing — resignFile is a named alias
1732
+ // that makes the caller's intent explicit at the call-site.
1733
+ return this.signFile(file, options);
1734
+ }
1735
+ /**
1736
+ * Extract metadata from a file's embedded signature without verifying it.
1737
+ *
1738
+ * Useful for rendering "Signed by X at Y" in a UI before the user
1739
+ * explicitly triggers a verify step, or for routing to the correct
1740
+ * contact record before calling verifyFile().
1741
+ *
1742
+ * Returns null if the file has no embedded signature or the JSON is
1743
+ * structurally malformed.
1744
+ *
1745
+ * @example
1746
+ * const info = await majik.getFileSignatureInfo(pdfBlob);
1747
+ * if (info) {
1748
+ * const contact = majik.getContactByID(info.signerId);
1749
+ * console.log(`Signed by ${contact?.meta?.label ?? info.signerId}`);
1750
+ * console.log(`Format handled by: ${info.handler}`);
1751
+ * }
1752
+ */
1753
+ async getFileSignatureInfo(file, options) {
1754
+ try {
1755
+ return MajikSignature.extractFrom(file, options);
1756
+ }
1757
+ catch (err) {
1758
+ this.emit("error", err, { context: "getFileSignatureInfo" });
1759
+ throw err;
1760
+ }
1761
+ }
1390
1762
  // ── Private: Signer resolution ────────────────────────────────────────────
1391
1763
  /**
1392
1764
  * Resolve MajikSignerPublicKeys from whichever signer hint was provided.
@@ -1626,6 +1998,12 @@ export class MajikMessage {
1626
1998
  catch {
1627
1999
  /* ignore */
1628
2000
  }
2001
+ try {
2002
+ await MajikKeyStore.deleteAll();
2003
+ }
2004
+ catch {
2005
+ /* ignore */
2006
+ }
1629
2007
  this.pinHash = null;
1630
2008
  this.id = arrayToBase64(randomBytes(32));
1631
2009
  try {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@majikah/majik-message",
3
3
  "type": "module",
4
4
  "description": "Post-quantum end-to-end encryption with ML-KEM-768. Seed phrase–based accounts. Auto-expiring messages. Offline-ready. Exportable encrypted messages. Tamper-proof threads with blockchain-like integrity. Quantum-resistant messaging.",
5
- "version": "0.2.5",
5
+ "version": "0.2.8",
6
6
  "license": "Apache-2.0",
7
7
  "author": "Zelijah",
8
8
  "main": "./dist/index.js",
@@ -81,9 +81,9 @@
81
81
  "dependencies": {
82
82
  "@bokuweb/zstd-wasm": "^0.0.27",
83
83
  "@majikah/majik-envelope": "^0.0.1",
84
- "@majikah/majik-file": "^0.0.17",
85
- "@majikah/majik-key": "^0.2.0",
86
- "@majikah/majik-signature": "^0.0.3",
84
+ "@majikah/majik-file": "^0.0.19",
85
+ "@majikah/majik-key": "^0.2.1",
86
+ "@majikah/majik-signature": "^0.0.6",
87
87
  "@noble/hashes": "^2.0.1",
88
88
  "@noble/post-quantum": "^0.5.4",
89
89
  "@scure/bip39": "^1.6.0",