@majikah/majik-message 0.2.5 → 0.2.6
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/majik-message.d.ts +255 -0
- package/dist/majik-message.js +372 -0
- package/package.json +1 -1
package/dist/majik-message.d.ts
CHANGED
|
@@ -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).
|
package/dist/majik-message.js
CHANGED
|
@@ -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.
|
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
|
+
"version": "0.2.6",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"author": "Zelijah",
|
|
8
8
|
"main": "./dist/index.js",
|