@majikah/majik-signature 0.2.7 → 0.2.9

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/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Majik Signature
2
2
 
3
3
  [![Developed by Zelijah](https://img.shields.io/badge/Developed%20by-Zelijah-red?logo=github&logoColor=white)](https://thezelijah.world) ![GitHub Sponsors](https://img.shields.io/github/sponsors/jedlsf?style=plastic&label=Sponsors&link=https%3A%2F%2Fgithub.com%2Fsponsors%2Fjedlsf)
4
- ![npm](https://img.shields.io/npm/v/@majikah/majik-signature) ![npm downloads](https://img.shields.io/npm/dm/@majikah/majik-signature) ![npm bundle size](https://img.shields.io/bundlephobia/min/%40majikah%2Fmajik-signature) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue)
4
+ ![npm](https://img.shields.io/npm/v/@majikah/majik-signature) ![npm downloads](https://img.shields.io/npm/dm/@majikah/majik-signature) ![npm bundle size](https://img.shields.io/bundlephobia/min/%40majikah%2Fmajik-signature) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue) [![Static Badge](https://img.shields.io/badge/IANA-vnd.majikah.mjksig-green)](https://www.iana.org/assignments/media-types/application/vnd.majikah.mjksig)
5
5
 
6
6
  **Majik Signature** is a hybrid post-quantum content signing and verification library for the Majikah ecosystem. Built on top of [**Majik Key**](https://www.npmjs.com/package/@majikah/majik-key), it produces tamper-evident, forgery-resistant digital signatures for any content — plaintext, JSON, PDFs, audio, video, Office documents, or raw binary — using a dual-algorithm architecture that combines classical **Ed25519** with post-quantum **ML-DSA-87** (FIPS-204).
7
7
 
8
- Beyond signing raw bytes, Majik Signature can **embed signatures directly into a file's native format**. PDFs stay PDFs, WAVs stay WAVs, DOCX files stay editable — the signature travels with the file, no sidecar `.sig` file required. On top of that, it supports **multi-party signing**, **signing allowlists**, **envelope sealing**, and **trusted timestamps (TSA)**.
8
+ Beyond signing raw bytes, Majik Signature can **embed signatures directly into a file's native format**, or produce **detached envelopes** that travel independently of the file — as portable JSON, base64, or a dedicated self-describing binary container (`.mjksig`). It supports **multi-party signing**, **signing allowlists**, **envelope sealing**, **trusted timestamps (TSA)**, **batch/folder signing** via a manifest format (`.mjksmap`), **chronological signing-order verification**, and **blockchain anchor registration**.
9
9
 
10
10
  ---
11
11
 
@@ -19,19 +19,28 @@ Beyond signing raw bytes, Majik Signature can **embed signatures directly into a
19
19
  - [Quick Start](#quick-start)
20
20
  - [Signing Raw Content](#signing-raw-content)
21
21
  - [File Embedding](#file-embedding)
22
+ - [Detached Signing](#detached-signing)
22
23
  - [Multi-Signature Files & Allowlists](#multi-signature-files--allowlists)
23
24
  - [Sealing an Envelope](#sealing-an-envelope)
24
25
  - [Trusted Timestamps (TSA)](#trusted-timestamps-tsa)
26
+ - [Batch Signing a Folder](#batch-signing-a-folder)
27
+ - [Verifying Signing Order](#verifying-signing-order)
28
+ - [Chain Anchoring](#chain-anchoring)
25
29
  - [API Reference](#api-reference)
26
30
  - [Content Signing](#content-signing-bytesstrings)
27
31
  - [File Embedding](#file-embedding-api)
32
+ - [Detached Signing & MajikSignatureEnvelope](#detached-signing--majiksignatureenvelope-api)
28
33
  - [Multi-Signature & Allowlist](#multi-signature--allowlist-api)
29
34
  - [Sealing](#sealing-api)
30
35
  - [Trusted Timestamps](#trusted-timestamps-api)
36
+ - [Batch Signing & MajikSignatureMap](#batch-signing--majiksignaturemap-api)
37
+ - [Signature Order Verification](#signature-order-verification-api)
38
+ - [Chain Anchoring API](#chain-anchoring-api)
31
39
  - [Image Stamping (Experimental)](#image-stamping-experimental)
32
40
  - [Serialization](#serialization)
33
41
  - [Supported File Formats](#supported-file-formats)
34
42
  - [Signature & Envelope Structure](#signature--envelope-structure)
43
+ - [Binary Container Formats (MJKSIG / MJKSMAP)](#binary-container-formats-mjksig--mjksmap)
35
44
  - [Error Handling](#error-handling)
36
45
  - [Security Considerations](#security-considerations)
37
46
  - [The Majikah Ecosystem](#the-majikah-ecosystem)
@@ -48,8 +57,9 @@ Beyond signing raw bytes, Majik Signature can **embed signatures directly into a
48
57
  Most "digital signature" libraries only sign raw bytes and leave you to figure out storage, transport, and file compatibility yourself. Majik Signature is built to be dropped into a real application:
49
58
 
50
59
  - **Hybrid post-quantum by default** — every signature is Ed25519 + ML-DSA-87, not an opt-in extra.
51
- - **Signatures live inside the file** — no sidecar files to lose, mismatch, or forget to ship.
52
- - **Multi-party signing is a first-class concept** — not bolted on. Allowlists, sealing, and issuer semantics are part of the core envelope format.
60
+ - **Signatures live inside the file, or travel detached** — embed directly into the file's native format, or produce a portable envelope (JSON, base64, or the self-describing `.mjksig` binary container) for out-of-band verification workflows.
61
+ - **Multi-party signing is a first-class concept** — not bolted on. Allowlists, sealing, issuer semantics, and chronological order verification are part of the core envelope model.
62
+ - **Batch-aware** — sign or verify an entire folder or zip's contents against a single manifest (`.mjksmap`) instead of tracking one signature file per asset.
53
63
  - **Deterministic by design** — the library goes out of its way to guarantee that signing and verifying the same content always produces the same canonical bytes (see [Signature & Envelope Structure](#signature--envelope-structure)).
54
64
  - **No native dependencies** — pure TypeScript/WASM-free cryptography, works identically in Node.js, browsers, Tauri, Deno, and Bun.
55
65
 
@@ -57,7 +67,6 @@ Most "digital signature" libraries only sign raw bytes and leave you to figure o
57
67
 
58
68
  ## Security Architecture
59
69
 
60
-
61
70
  ```mermaid
62
71
  flowchart TD
63
72
  A[12/24-word BIP-39 Seed Phrase] --> B[Majik Key]
@@ -67,28 +76,16 @@ flowchart TD
67
76
  S --> S1[Ed25519]
68
77
  S --> S2[ML-DSA-87]
69
78
 
70
-
71
-
72
79
  %% Identity branch
73
80
  B --> I[Identity]
74
81
  I --> I1[BIP-39]
75
82
  I --> I2[X25519]
76
83
 
77
-
78
-
79
84
  %% Products (fan-in)
80
85
  S1 --> P1[Majik Signature]
81
86
  S2 --> P1
82
-
83
-
84
-
85
-
86
-
87
-
88
-
89
87
  ```
90
88
 
91
-
92
89
  ### 1. Hybrid Dual-Algorithm Signing
93
90
 
94
91
  Every Majik Signature is produced by **two independent signing algorithms** over the same canonical payload:
@@ -126,7 +123,7 @@ This binding means a valid signature cannot be reused on different content, tran
126
123
 
127
124
  Content is never embedded in the envelope — only its SHA-256 hash is signed. A 500 MB video signs at the same speed as a 10-byte string, and every content type is supported identically.
128
125
 
129
- ### 4. File Embedding Integrity
126
+ ### 4. File Embedding & Detachment Integrity
130
127
 
131
128
  When a signature is embedded into a file, it always covers the **original file bytes before embedding**. Verification strips the embedded envelope before re-hashing, so the round-trip is always:
132
129
 
@@ -134,11 +131,13 @@ When a signature is embedded into a file, it always covers the **original file b
134
131
  sign(originalBytes) → embed into file → extract → strip → verify(originalBytes)
135
132
  ```
136
133
 
134
+ The same guarantee applies to **detached** signing: `signFileDetached()` signs the clean, stripped bytes and returns them alongside the envelope — the envelope and the file travel separately, but verification always strips the file first, so an accidentally-still-embedded envelope never double-counts or corrupts the hash.
135
+
137
136
  Re-signing (or re-embedding) the same file is always safe — the existing envelope is stripped before the new one is written, so signatures never stack or corrupt each other.
138
137
 
139
138
  ### 5. Multi-Party Signing, Allowlists & Sealing
140
139
 
141
- Files don't hold a single signature — they hold a **`MultiSigEnvelope`**: an array of per-signer envelopes, plus optional allowlist and seal metadata.
140
+ Files don't hold a single signature — they hold a **`MultiSigEnvelope`** (modeled at runtime by the `MajikSignatureEnvelope` class): an array of per-signer envelopes, plus optional allowlist, seal, and chain-anchor metadata.
142
141
 
143
142
  - **Open signing** (default): anyone with a `MajikKey` can add a signature.
144
143
  - **Restricted signing**: the first signer may supply an `expectedSigners` allowlist. That allowlist is cryptographically committed to via `allowlistHash` in the issuer's own canonical payload — tampering with the allowlist after the fact breaks the issuer's signature. Non-listed signers are rejected *before* any cryptographic operation runs.
@@ -146,7 +145,11 @@ Files don't hold a single signature — they hold a **`MultiSigEnvelope`**: an a
146
145
 
147
146
  ### 6. Trusted Timestamps (TSA)
148
147
 
149
- A `MajikSignature` can optionally carry a `MajikTimestamp` — a signature from a timestamp authority over a canonical payload (`"majikah-tsa-v-1:"` domain) binding a digest, a server-generated nonce, and a server-authoritative timestamp. The TSA signature is itself a full Majik Signature (Ed25519 + ML-DSA-87), so it inherits the same hybrid guarantees.
148
+ A `MajikSignature` can optionally carry a `MajikTimestamp` — a signature from a timestamp authority over a canonical payload (`"majikah-tsa-v-1:"` domain) binding a digest, a server-generated nonce, and a server-authoritative timestamp. The TSA signature is itself a full Majik Signature (Ed25519 + ML-DSA-87), so it inherits the same hybrid guarantees. TSA timestamps are also what power *attested* [signing-order verification](#verifying-signing-order) — see below.
149
+
150
+ ### 7. Chain Anchoring
151
+
152
+ A sealed envelope's seal hash can be committed to an external blockchain (handled by a companion product, e.g. `majik-notary`) and the resulting confirmed anchor registered back into the envelope via `MajikChainAnchor` records. The SDK does not talk to any chain itself — it only builds the canonical memo to anchor (`buildChainAnchorMemo()`) and embeds/reads already-confirmed anchors. Anchoring requires the envelope to already be sealed.
150
153
 
151
154
  ---
152
155
 
@@ -163,10 +166,12 @@ Verification is fully **public** — anyone with the signer's public keys can ve
163
166
  - **Content Provenance** — prove that music, art, a document, or a dataset was produced by a specific identity.
164
167
  - **File Integrity** — detect any tampering or modification to distributed files.
165
168
  - **API Payload Signing** — sign JSON requests/responses for non-repudiation.
166
- - **Document Authentication** — certify contracts, legal records, or invoices, with support for multiple independent signers and a final seal.
169
+ - **Document Authentication** — certify contracts, legal records, or invoices, with support for multiple independent signers, chronological approval order, and a final seal.
167
170
  - **Media Certification** — stamp audio, video, or image files as authentic originals, with the signature embedded directly in the file.
168
171
  - **Software Distribution** — sign release artifacts to prove they come from the original author.
169
172
  - **Identity-Bound Messaging** — bind signed content to a verifiable identity across the Majikah ecosystem.
173
+ - **Batch Asset Signing** — sign every file in a folder, project export, or zip archive against a single manifest, resilient to files being renamed or relocated afterward.
174
+ - **Approval Workflows** — verify not just *who* signed a contract, but that they signed it in the *required order* (e.g. legal before finance, requester before approver).
170
175
 
171
176
  ---
172
177
 
@@ -176,28 +181,40 @@ Verification is fully **public** — anyone with the signer's public keys can ve
176
181
 
177
182
  - **Hybrid signatures** — Ed25519 (classical) + ML-DSA-87 (post-quantum, FIPS-204, Category 5); both must verify
178
183
  - **Tamper detection** — SHA-256 content hash is bound inside the signed payload; any byte change invalidates both signatures
179
- - **Domain separation** — distinct prefixes for signing (`majik-signature-v1:`), sealing (`majik-seal-v1:`), and timestamping (`majikah-tsa-v-1:`) prevent cross-protocol signature reuse
184
+ - **Domain separation** — distinct prefixes for signing (`majik-signature-v1:`), sealing (`majik-seal-v1:`), timestamping (`majikah-tsa-v-1:`), and chain anchoring (`majik-notary-v-1:`) prevent cross-protocol signature reuse
180
185
  - **Signer & timestamp binding** — both are part of the signed payload; neither can be altered or transferred after signing
181
186
  - **No private key for verification** — pure public-key verification, safe to run anywhere
182
187
 
183
188
  ### Multi-Party Signing
184
189
 
185
- - **Multi-signature envelopes** — any number of independent signers on one file
190
+ - **Multi-signature envelopes** — any number of independent signers on one file, modeled by the immutable `MajikSignatureEnvelope` class
186
191
  - **Signing allowlists** — restrict who may sign, enforced before any cryptographic operation
187
192
  - **Cryptographically committed allowlists** — tampering with the allowlist invalidates the issuer's signature
188
193
  - **Sealing** — the issuer can permanently lock an envelope against further signatures
194
+ - **Chronological order verification** — verify that signers signed in a required sequence, with TSA-attested timestamps preferred over self-reported ones, and a strict mode to reject unexpected extra signers
189
195
  - **Status queries** — `getSignatories()`, `getIssuer()`, `getEnvelopeInfo()`, `canSign()` for building signing-status UI without manually walking the envelope
190
196
 
197
+ ### Detached Signing & Batch Workflows
198
+
199
+ [![Static Badge](https://img.shields.io/badge/IANA-vnd.majikah.mjksig-green)](https://www.iana.org/assignments/media-types/application/vnd.majikah.mjksig)
200
+
201
+ - **Detached envelopes** — sign a file and receive the envelope separately, for external verification pipelines where payload and signature travel independently
202
+ - **Self-describing binary containers** — `.mjksig` for a single detached envelope, `.mjksmap` for a manifest covering an entire batch, each with magic bytes, a version header, and a length-prefixed payload
203
+ - **Batch signing** — sign every file in a folder or zip in one call, packaged as one `.mjksmap` manifest or as separate `.mjksig` files per asset
204
+ - **Relocation-tolerant batch verification** — a file renamed or moved after signing is still found and verified by content hash, not just by its original path
205
+ - **Per-file batch verification reporting** — `verified` / `invalid` / `tampered` / `not_in_map` status per file, plus a one-glance summary
206
+
191
207
  ### Content Format Support
192
208
 
193
209
  - **Plain text, JSON, binary** — `Uint8Array` or `string`
194
210
  - **PDF** — signature appended as a spec-compliant binary trailer after the file's `%%EOF`
195
- - **PNG** — embedded in a `iTXt` metadata chunk
211
+ - **PNG** — embedded in an `iTXt` metadata chunk
196
212
  - **WAV** — embedded in a RIFF `LIST INFO` chunk
197
213
  - **MP3** — embedded in an ID3v2 `TXXX` frame
198
- - **MP4, MOV, M4A, M4V** — embedded in the `moov/udta` box
214
+ - **MP4, MOV, M4A, M4V** — embedded in the `moov/udta` box, custom `majk` box type
199
215
  - **DOCX, XLSX, PPTX, ODT, ODS, ODP** — embedded as a dedicated entry inside the ZIP container
200
216
  - **MKV, WebM** — embedded via a custom Matroska metadata tag
217
+ - **JPEG, FLAC** — native format metadata, same round-trip guarantees as other handlers
201
218
  - **HTML, Markdown, JSON, plain text, source code** — embedded as an appended, format-appropriate metadata block
202
219
  - **Any other format** — universal binary trailer: `[original bytes][signature JSON][8-byte length][8-byte magic]` — cleanly detectable and strippable regardless of format
203
220
 
@@ -206,19 +223,21 @@ See [Supported File Formats](#supported-file-formats) for the full handler table
206
223
  ### Developer Experience
207
224
 
208
225
  - **First-class TypeScript support** — full type definitions for every interface and class
209
- - **Simple core API** — `sign()` / `verify()` for bytes and strings; `signFile()` / `verifyFile()` for files
226
+ - **Simple core API** — `sign()` / `verify()` for bytes and strings; `signFile()` / `verifyFile()` for embedded files; `signFileDetached()` / `verifyFileDetached()` for detached workflows
210
227
  - **One-liner file signing** — `MajikSignature.signFile(blob, key)` signs and embeds in a single call
211
228
  - **Format auto-detection** — MIME type and magic-byte sniffing, no manual format hints required in most cases
212
229
  - **Idempotent re-signing** — safely re-sign or re-embed any file without accumulating stacked or orphaned envelopes
230
+ - **Immutable envelope model** — `MajikSignatureEnvelope` and `MajikSignatureMap` are both immutable; every mutation returns a new instance
213
231
  - **Typed error hierarchy** — precise, catchable error classes instead of generic exceptions
214
232
  - **Isomorphic** — Node.js, browsers, Tauri, Deno, and Bun; no native bindings
215
233
 
216
234
  ### Serialization & Portability
217
235
 
218
- - **JSON envelope** — full `toJSON()` / `fromJSON()` round-trip
236
+ - **JSON envelope** — full `toJSON()` / `fromJSON()` round-trip, at both the signature and envelope level
219
237
  - **Base64 serialization** — `serialize()` / `deserialize()` for compact transport (HTTP headers, DB columns, etc.)
220
238
  - **File-embedded** — the signature lives inside the file itself, no sidecar files needed
221
239
  - **Self-contained** — the envelope includes the signer's public keys, verifiable without a separate key registry
240
+ - **Binary containers** — `.mjksig` and `.mjksmap` for detached envelopes and batch manifests that need to travel or be stored on disk as their own file
222
241
 
223
242
  ---
224
243
 
@@ -303,6 +322,42 @@ const originalBlob = await MajikSignature.stripFrom(signedBlob);
303
322
 
304
323
  ---
305
324
 
325
+ ### Detached Signing
326
+
327
+ Use detached signing when the signature envelope needs to travel independently of the file — for example, storing envelopes in a database while files sit in blob storage, or verification pipelines that never touch the original file storage layer.
328
+
329
+ ```typescript
330
+ import { MajikSignature } from '@majikah/majik-signature';
331
+
332
+ // Sign — returns the clean file bytes AND the envelope, separately
333
+ const { blob, envelope, signature } = await MajikSignature.signFileDetached(file, aliceKey);
334
+
335
+ // The envelope can be stored/transported however you like:
336
+ const envelopeJson = envelope.toJSON();
337
+ const envelopeB64 = envelope.serialize();
338
+ const envelopeBinary = envelope.toMJKSIG(); // a portable .mjksig Blob
339
+
340
+ // Verify later, against the same clean file bytes + the detached envelope
341
+ const results = await MajikSignature.verifyFileDetached(blob, envelope, aliceKey);
342
+ console.log('Valid:', results.every((r) => r.valid));
343
+
344
+ // Multiple signers can be added to the same detached envelope before it's ever embedded
345
+ const { envelope: envelope2 } = await MajikSignature.signFileDetached(blob, bobKey, {
346
+ existingEnvelope: envelope, // continue from Alice's envelope
347
+ });
348
+ ```
349
+
350
+ You can attach a Trusted Timestamp to a detached signature in the same call:
351
+
352
+ ```typescript
353
+ const { envelope, signature } = await MajikSignature.signFileDetached(file, aliceKey, {
354
+ tsa: myTsaTimestamp,
355
+ });
356
+ console.log(signature.hasTSA); // true
357
+ ```
358
+
359
+ ---
360
+
306
361
  ### Multi-Signature Files & Allowlists
307
362
 
308
363
  ```typescript
@@ -385,6 +440,111 @@ console.log('TSA still valid:', signature.verifyTSA().valid);
385
440
 
386
441
  ---
387
442
 
443
+ ### Batch Signing a Folder
444
+
445
+ Sign every file in a folder, project export, or zip's contents in one call, packaged as a single manifest.
446
+
447
+ ```typescript
448
+ import { MajikSignature } from '@majikah/majik-signature';
449
+
450
+ const result = await MajikSignature.signBatchDetached(
451
+ [
452
+ { path: 'docs/report.pdf', blob: reportBlob },
453
+ { path: 'docs/appendix.pdf', blob: appendixBlob },
454
+ { path: 'assets/cover.png', blob: coverBlob },
455
+ ],
456
+ aliceKey,
457
+ );
458
+
459
+ if (result.mode === 'map') {
460
+ // result.map is a MajikSignatureMap instance; result.mapBlob is a ready-to-store .mjksmap Blob
461
+ zip.file('signatures.mjksmap', await result.mapBlob.arrayBuffer());
462
+ }
463
+
464
+ // Later — verify an extracted batch against the manifest
465
+ const map = await MajikSignature.getSignatureMapFromMJKSMAP(mapBlob);
466
+ // (or: const map = await MajikSignatureMap.fromMJKSMAP(mapBlob);)
467
+
468
+ const results = await MajikSignature.verifyFilesFromMjksMap(
469
+ map,
470
+ extractedFiles, // { path, blob }[]
471
+ publicKeys,
472
+ );
473
+
474
+ const summary = MajikSignature.summarizeBatchVerification(results);
475
+ console.log(`${summary.verified}/${summary.total} verified`);
476
+
477
+ if (!summary.allValid) {
478
+ for (const r of results) {
479
+ if (r.status !== 'verified') console.log(r.path, r.status, r.reason);
480
+ }
481
+ }
482
+ ```
483
+
484
+ A batch-verified file that was renamed or moved after signing is still resolved correctly — matched by content hash and reported with `relocatedFrom` set to its original path, rather than showing up as missing.
485
+
486
+ ---
487
+
488
+ ### Verifying Signing Order
489
+
490
+ Beyond confirming *who* signed a file, you can confirm they signed in a required *sequence* — e.g. "Bob must sign before Dave." TSA-attested timestamps are preferred automatically when present; self-reported timestamps are used as a fallback and flagged accordingly.
491
+
492
+ ```typescript
493
+ import { MajikSignature } from '@majikah/majik-signature';
494
+
495
+ // expectedOrder accepts MajikKey instances and/or ExpectedSigner objects, mixed freely
496
+ const result = await MajikSignature.verifyFileOrder(signedBlob, [bobKey, daveKey]);
497
+
498
+ if (!result.allExpectedSigned) {
499
+ console.log('Still pending:', result.pendingSigners);
500
+ } else if (!result.allValid) {
501
+ console.log('Invalid/tampered signature(s):', result.invalidSigners);
502
+ } else if (!result.orderRespected) {
503
+ console.log('Signed out of order:', result.violations);
504
+ } else if (result.softTieWarnings.length > 0) {
505
+ console.log('Valid, but note:', result.reason); // identical timestamps — order indistinguishable
506
+ } else {
507
+ console.log('Signed in the expected order — all valid.');
508
+ }
509
+
510
+ if (result.usesUnattestedTimestamp) {
511
+ console.log('Note: order relied on at least one self-reported (non-TSA) timestamp.');
512
+ }
513
+
514
+ // Strict mode — fail if anyone outside expectedOrder signed at all
515
+ const strictResult = await MajikSignature.verifyFileOrder(
516
+ signedBlob,
517
+ [bobKey, daveKey],
518
+ { strict: true },
519
+ );
520
+ ```
521
+
522
+ The same check works against a detached envelope via `verifyFileDetachedOrder(file, envelope, expectedOrder, options?)`.
523
+
524
+ ---
525
+
526
+ ### Chain Anchoring
527
+
528
+ Chain anchoring is a two-step handoff: this SDK prepares and reads anchor data; a separate chain-integration layer (e.g. `majik-notary`) is responsible for actually submitting and confirming the transaction.
529
+
530
+ ```typescript
531
+ import { MajikSignature } from '@majikah/majik-signature';
532
+
533
+ // 1. File must be sealed first
534
+ const { permitted, reason } = await MajikSignature.canAnchor(sealedBlob);
535
+
536
+ // 2. Build the canonical memo to submit on-chain (elsewhere, e.g. via majik-notary)
537
+ const memo = MajikSignature.buildChainAnchorMemo(sealInfo.sealHash);
538
+
539
+ // 3. Once the external chain integration confirms the transaction, register the anchor
540
+ const blob = await MajikSignature.registerChainAnchor(sealedBlob, confirmedAnchor);
541
+
542
+ // 4. Read anchors back later
543
+ const anchors = await MajikSignature.getChainAnchors(blob);
544
+ ```
545
+
546
+ ---
547
+
388
548
  ## API Reference
389
549
 
390
550
  ### Content Signing (bytes/strings)
@@ -398,6 +558,7 @@ Sign raw bytes or a string with an unlocked `MajikKey`.
398
558
  - `options?: SignOptions`
399
559
  - `contentType?: string` — advisory label (see `CONTENT_TYPES`)
400
560
  - `timestamp?: string` — ISO 8601 override (defaults to `new Date().toISOString()`)
561
+ - `expectedSigners?: ExpectedSigner[]` — accepted at the type level for allowlist-establishing flows; ignored by the bare `sign()` call itself (only meaningful via the file-level allowlist APIs)
401
562
 
402
563
  **Returns:** `Promise<MajikSignature>`
403
564
  **Throws:** `MajikSignatureKeyError` if the key is locked or has no signing keys.
@@ -417,7 +578,8 @@ Verify a signature against content and the signer's public keys. Both Ed25519 an
417
578
  contentHash?: string;
418
579
  timestamp: string;
419
580
  contentType?: string;
420
- reason?: string; // present when valid is false
581
+ handler?: string; // present when result came from a file-level verify
582
+ reason?: string; // present when valid is false
421
583
  }
422
584
  ```
423
585
 
@@ -429,6 +591,14 @@ Convenience wrapper — verifies directly against a `MajikKey` instance. Works e
429
591
  #### `MajikSignature.publicKeysFromMajikKey(key)`
430
592
  Extracts `{ signerId, edPublicKey, mlDsaPublicKey }` from a `MajikKey` for use with `verify()`. Works on locked keys.
431
593
 
594
+ #### `signature.extractPublicKeys()` *(instance method)*
595
+ Extracts `{ signerId, edPublicKey, mlDsaPublicKey }` **from the signature envelope itself**, rather than from a `MajikKey`. Useful when you only have a serialized signature and need its embedded public keys — for example, to independently verify a TSA token via `verifyTSA()`. Validates key lengths before returning.
596
+
597
+ > ⚠️ Keys extracted this way are **self-asserted by the envelope** — always cross-check the returned `signerId` against a trusted source before relying on the result. See [Security Considerations](#security-considerations).
598
+
599
+ #### `signature.validate()` / `signature.isValid()`
600
+ `validate()` re-runs structural validation on the signature's own JSON shape and throws on failure; `isValid()` is the non-throwing boolean wrapper. Useful as a cheap sanity check after deserializing from untrusted storage, before running full cryptographic verification.
601
+
432
602
  #### `MajikSignature.fromJSON(json)` / `MajikSignature.deserialize(base64)`
433
603
  Reconstruct a `MajikSignature` instance from stored JSON or a base64 string.
434
604
 
@@ -450,7 +620,7 @@ Sign a file and embed the signature in one call. Strips any existing envelope fi
450
620
  - `mimeType?: string` — override auto-detected MIME type
451
621
  - `expectedSigners?: ExpectedSigner[]` — only honored on the **first** signature; establishes a signing allowlist
452
622
 
453
- **Returns:** `Promise<{ blob: Blob; signature: MajikSignature; handler: string; mimeType: string }>`
623
+ **Returns:** `Promise<{ blob: Blob; signature: MajikSignature; envelope: MajikSignatureEnvelope; handler: string; mimeType: string }>`
454
624
 
455
625
  ---
456
626
 
@@ -487,10 +657,55 @@ Structural presence check — does the file contain an envelope at all? Does not
487
657
 
488
658
  ---
489
659
 
660
+ ### Detached Signing & MajikSignatureEnvelope API
661
+
662
+ #### `MajikSignature.signFileDetached(file, key, options?)`
663
+
664
+ Sign a file and return the resulting envelope **detached** — the returned `blob` is the clean, stripped file; the envelope travels separately.
665
+
666
+ - `options?.existingEnvelope?: MajikSignatureEnvelope | MajikSignatureEnvelopeJSON | Uint8Array | Blob` — continue signing an envelope that started out-of-band (any of: an instance, its JSON shape, raw `.mjksig` bytes, or a `.mjksig` Blob)
667
+ - `options?.expectedSigners?: ExpectedSigner[]` — same allowlist-establishing semantics as `signFile()`
668
+ - `options?.tsa?: MajikTimestamp` — attach a Trusted Timestamp to this signer's entry before it's added to the envelope
669
+
670
+ **Returns:** `Promise<{ blob: Blob; envelope: MajikSignatureEnvelope; signature: MajikSignature; handler: string; mimeType: string }>`
671
+
672
+ #### `MajikSignature.verifyFileDetached(file, envelope, keyOrPublicKeys, options?)`
673
+ Verify a file against a detached envelope (instance, JSON, `.mjksig` bytes, or Blob — accepted via the same flexible shape as `existingEnvelope` above). Strips the file first, in case it also happens to carry an embedded envelope.
674
+
675
+ **Returns:** `Promise<VerificationResult[]>`
676
+
677
+ ---
678
+
679
+ #### `MajikSignatureEnvelope`
680
+
681
+ The behavior-rich, immutable in-memory counterpart to the wire-format `MultiSigEnvelope` JSON. Every `with*` method returns a **new** instance — the receiver is never mutated. This is the class returned by `signFileDetached()`, accepted by `verifyFileDetached()`, and underlying every multi-sig query method.
682
+
683
+ **State predicates:** `isSealed()`, `hasAllowlist()`, `isFirstSigner()`, `isMultiSig()`, `hasMultipleSignatories()`, `isIssuer(keyOrFingerprint)`
684
+
685
+ **Lookup:** `findSignature(signerId)`, `get signatures`, `get allowlist`, `get chainAnchors`
686
+
687
+ **Allowlist enforcement:** `checkAllowlist(key)`, `assertCanSign(key)` (throws), `canSign(key)` (non-throwing), `verifyAllowlistIntegrity()`
688
+
689
+ **Builders (immutable):** `withSignature(sig)`, `withAllowlist(allowlist, signerId)`, `withSeal(sealedBy, timestamp?)`, `withChainAnchor(anchor)`
690
+
691
+ **Seal queries:** `verifySeal()`, `getSealInfo()`, `canAnchor()`
692
+
693
+ **Signatory resolution:** `resolveIssuer()`, `getSignatories(filter?)`, `getEnvelopeInfo()`
694
+
695
+ **Serialization:** `toJSON()`, `serialize()` / `MajikSignatureEnvelope.deserialize(base64)`, `toMJKSIG()` / `toMJKSIGBytes()` / `MajikSignatureEnvelope.fromMJKSIG(input)`, `MajikSignatureEnvelope.isMJKSIG(input)`, `MajikSignatureEnvelope.getMJKSIGVersion(input)`
696
+
697
+ **Creation / parsing:** `MajikSignatureEnvelope.empty()`, `MajikSignatureEnvelope.fromJSON(json)` (also transparently promotes legacy bare single-sig JSON), `MajikSignatureEnvelope.from(input)` (accepts an instance, JSON, `.mjksig` bytes, or Blob — the universal entry point used internally by every detached-envelope-accepting method)
698
+
699
+ **Validation:** `validate()` (throws), `isValid()` (boolean)
700
+
701
+ See [Binary Container Formats](#binary-container-formats-mjksig--mjksmap) for details on `.mjksig`.
702
+
703
+ ---
704
+
490
705
  ### Multi-Signature & Allowlist API
491
706
 
492
707
  #### `MajikSignature.expectedSignerFromKey(key)`
493
- Builds an `ExpectedSigner` entry (`{ signerId, edPublicKey, mlDsaPublicKey }`) from a `MajikKey`, for use in `signFile()`'s `expectedSigners` option. The key does not need to be unlocked.
708
+ Builds an `ExpectedSigner` entry (`{ signerId, edPublicKey, mlDsaPublicKey }`) from a `MajikKey`, for use in `signFile()`'s / `signFileDetached()`'s `expectedSigners` option. The key does not need to be unlocked.
494
709
 
495
710
  #### `MajikSignature.getAllowlist(file, options?)`
496
711
  **Returns:** `Promise<ExpectedSigner[] | null>` — `null` for open-signing or unsigned files.
@@ -557,7 +772,7 @@ Builds a `MajikTSARequest` (`{ digest: { algorithm: "SHA-256", value } }`) from
557
772
  **Server-side.** Signs a TSA payload and returns a complete `MajikTimestamp`, including a server-generated nonce and timestamp.
558
773
 
559
774
  #### `signature.addTSA(timestamp)`
560
- Attaches and validates a `MajikTimestamp` on an existing signature instance. Throws `MajikSignatureError` if a TSA is already present, if the digest doesn't match, or `MajikSignatureVerificationError` if the TSA signature itself doesn't verify. A TSA, once attached, cannot be replaced.
775
+ Attaches and validates a `MajikTimestamp` on an existing signature instance. Throws `MajikSignatureError` if a TSA is already present or if the digest doesn't match, or `MajikSignatureVerificationError` if the TSA signature itself doesn't verify. A TSA, once attached, cannot be replaced.
561
776
 
562
777
  #### `signature.verifyTSA()`
563
778
  Re-verifies the attached TSA's own signature — useful after deserializing a signature from storage.
@@ -567,6 +782,131 @@ Re-verifies the attached TSA's own signature — useful after deserializing a si
567
782
 
568
783
  ---
569
784
 
785
+ ### Batch Signing & MajikSignatureMap API
786
+
787
+ #### `MajikSignature.signBatchDetached(files, key, options?)`
788
+
789
+ Sign a batch of files (e.g. a folder or zip's contents) as detached envelopes.
790
+
791
+ - `files: { path: string; blob: Blob }[]` — every path must be unique within the batch
792
+ - `options?.mode?: "map" | "separate"` — `"map"` (default) produces one `MajikSignatureMap` covering the whole batch; `"separate"` produces one `.mjksig` Blob per file
793
+ - `options?.continueOnError?: boolean` — default `false` (abort the whole batch on the first failure); set `true` to collect failures per-file and continue
794
+
795
+ **Returns:**
796
+ ```typescript
797
+ | { mode: "map"; map: MajikSignatureMap; mapBlob: Blob; failures: BatchSignFailure[] }
798
+ | { mode: "separate"; signatures: { path: string; blob: Blob }[]; failures: BatchSignFailure[] }
799
+ ```
800
+
801
+ #### `MajikSignature.verifyFilesFromMjksMap(map, files, publicKeys, options?)`
802
+ Verify a batch of extracted files against a `MajikSignatureMap`. Never throws per-file by default — every outcome (missing, tampered, relocated-but-valid, invalid, verified) is reported so you can render a full status table in one pass.
803
+
804
+ - `options?.expectedSignerId?: string`
805
+ - `options?.requireAllPresent?: boolean` — escalate a missing file to a thrown error instead of a per-file `"not_in_map"` result
806
+
807
+ **Returns:** `Promise<FileVerifyResult[]>`
808
+
809
+ ```typescript
810
+ {
811
+ path: string;
812
+ status: "verified" | "invalid" | "tampered" | "not_in_map";
813
+ results?: VerificationResult[];
814
+ reason?: string;
815
+ relocatedFrom?: string; // present only when found by content match at a different path
816
+ }
817
+ ```
818
+
819
+ #### `MajikSignature.verifyFilesFromMjksMapWithKey(map, files, key, options?)`
820
+ Convenience overload — resolves public keys from a `MajikKey` instead of requiring `MajikSignerPublicKeys` directly.
821
+
822
+ #### `MajikSignature.summarizeBatchVerification(results)`
823
+ One-glance pass/fail summary over a `FileVerifyResult[]`.
824
+
825
+ **Returns:** `{ total, verified, invalid, tampered, notInMap, allValid }`
826
+
827
+ ---
828
+
829
+ #### `MajikSignatureMap`
830
+
831
+ The behavior-rich, immutable class backing `.mjksmap`. Keyed by **path**, not content hash alone — duplicate-content files across a batch are legitimate and must not collide.
832
+
833
+ **Lookup:** `getEntry(path)`, `hasEntry(path)`, `findEntry(path, file)` (hash-verified against stored bytes), `findEntriesByHash(file)`, `resolveEntry(path, file)` (relocation-tolerant — tries the given path first, falls back to content match), `getEnvelope(path)`, `getAllEnvelopes()`
834
+
835
+ **Builders (immutable):** `withEntry(entry)`, `withoutEntry(path)`
836
+
837
+ **Serialization:** `toJSON()`, `toMJKSMAP()` / `toMJKSMAPBytes()` / `MajikSignatureMap.fromMJKSMAP(input)`, `MajikSignatureMap.isMJKSMAP(input)`
838
+
839
+ **Creation / parsing:** `MajikSignatureMap.empty()`, `MajikSignatureMap.fromJSON(json)`, `MajikSignatureMap.from(input)` (accepts an instance, JSON, `.mjksmap` bytes, or Blob)
840
+
841
+ **Validation:** `validate()` (throws), `isValid()` (boolean)
842
+
843
+ `resolveEntry()`'s status values: `"path_match"` (found, content unchanged), `"path_tampered"` (found at that path, content no longer matches), `"relocated"` (not at the given path, but found elsewhere by content hash), `"not_found"`.
844
+
845
+ See [Binary Container Formats](#binary-container-formats-mjksig--mjksmap) for details on `.mjksmap`.
846
+
847
+ ---
848
+
849
+ ### Signature Order Verification API
850
+
851
+ Verifies that a set of expected signers signed in a required chronological sequence. TSA-attested timestamps (`tsa.payload.timestamp`) are preferred automatically over self-reported ones (`timestamp`); every result flags whether any comparison relied on a self-reported, non-attested clock.
852
+
853
+ #### `MajikSignature.verifyFileOrder(file, expectedOrder, options?)`
854
+
855
+ - `expectedOrder: (MajikKey | ExpectedSigner)[]` — array position is the expected chronological position; `MajikKey` instances and `ExpectedSigner` objects can be mixed freely and are normalized internally
856
+ - `options?.strict?: boolean` — default `false`. When `true`, any signer present in the envelope but absent from `expectedOrder` fails the overall result
857
+
858
+ **Returns:** `Promise<SignatureOrderResult>`
859
+
860
+ ```typescript
861
+ {
862
+ valid: boolean; // true only if everyone expected signed, all valid, order respected (and, in strict mode, no extra signers)
863
+ allExpectedSigned: boolean;
864
+ allValid: boolean;
865
+ orderRespected: boolean;
866
+ strict: boolean;
867
+ unexpectedSigners: string[]; // populated only when strict is true
868
+ pendingSigners: string[];
869
+ invalidSigners: string[];
870
+ violations: OrderViolation[]; // { earlier, later, earlierTimestamp, laterTimestamp }
871
+ usesUnattestedTimestamp: boolean;
872
+ softTieWarnings: SoftTieWarning[]; // identical timestamps between two signers — order indistinguishable, doesn't fail `valid`
873
+ signers: SignerOrderStatus[];
874
+ reason?: string;
875
+ }
876
+ ```
877
+
878
+ #### `MajikSignature.verifyFileDetachedOrder(file, envelope, expectedOrder, options?)`
879
+ Same semantics as `verifyFileOrder()`, against a detached envelope (instance, JSON, `.mjksig` bytes, or Blob).
880
+
881
+ #### `MajikSignature.normalizeExpectedOrder(expectedOrder)`
882
+ Normalizes a mixed array of `MajikKey` instances and/or `ExpectedSigner` objects into a plain `ExpectedSigner[]`. Exposed standalone in case you want to build and cache the normalized order ahead of time, without immediately verifying.
883
+
884
+ > Order comparisons only run between signers who **both** signed and **both** produced a valid signature — an invalid signature's timestamp isn't trustworthy, so it's excluded from the ordering check but still reported via `invalidSigners`. Order verification checks the relative sequence of your `expectedOrder` list; in non-strict mode it does not detect an unlisted signer signing *between* two expected signers — use `strict: true` if the presence of any signer outside your expected set must itself be treated as a failure.
885
+
886
+ ---
887
+
888
+ ### Chain Anchoring API (Experimental)
889
+
890
+ > ⚠️ **These APIs are explicitly marked experimental in source and are not yet API-stable.** Expect breaking changes across minor versions.
891
+
892
+ #### `MajikSignature.canAnchor(file, options?)`
893
+ Checks whether a file is eligible for chain anchoring (requires the envelope to be sealed).
894
+
895
+ **Returns:** `Promise<{ permitted: boolean; reason?: string }>`
896
+
897
+ #### `MajikSignature.buildChainAnchorMemo(sealHash)`
898
+ Builds the canonical, domain-separated memo string (`"majik-notary-v-1:" + sealHash`) intended to be submitted on-chain by an external integration layer. Does not talk to any chain itself.
899
+
900
+ #### `MajikSignature.registerChainAnchor(file, anchor, options?)`
901
+ Embeds an **already-confirmed** `MajikChainAnchor` into the file's envelope. The caller is responsible for submitting and confirming the transaction beforehand (e.g. via a separate chain-integration product). Upserts by `anchor.id`, so retried registration doesn't produce duplicates.
902
+
903
+ **Returns:** `Promise<Blob>`
904
+
905
+ #### `MajikSignature.getChainAnchors(file, options?)`
906
+ **Returns:** `Promise<MajikChainAnchor[]>` — empty array if none, or if the file has no envelope.
907
+
908
+ ---
909
+
570
910
  ### Image Stamping (Experimental)
571
911
 
572
912
  > ⚠️ **These APIs are explicitly marked experimental in source and are not yet API-stable.** Expect breaking changes across minor versions.
@@ -583,6 +923,15 @@ Full round-trip to/from the `MajikSignatureJSON` shape.
583
923
  #### `signature.serialize()` / `MajikSignature.deserialize(base64)`
584
924
  Compact base64 transport format — useful for HTTP headers or database columns.
585
925
 
926
+ #### `envelope.toJSON()` / `MajikSignatureEnvelope.fromJSON(json)`
927
+ Full round-trip to/from the `MultiSigEnvelope` shape, including transparent promotion of legacy bare single-sig JSON.
928
+
929
+ #### `envelope.serialize()` / `MajikSignatureEnvelope.deserialize(base64)`
930
+ Base64 transport for a full multi-sig envelope.
931
+
932
+ #### `envelope.toMJKSIG()` / `MajikSignatureEnvelope.fromMJKSIG(input)`
933
+ Self-describing binary container for a detached envelope — see [Binary Container Formats](#binary-container-formats-mjksig--mjksmap).
934
+
586
935
  ```typescript
587
936
  const signature = await MajikSignature.sign(content, key);
588
937
 
@@ -615,7 +964,9 @@ Handlers are tried in order; the first one whose `canHandle()` matches wins. If
615
964
  | HTML, Markdown, JSON, plain text, source code | Text | Appended, format-appropriate metadata block |
616
965
  | Anything else | Fallback | Universal binary trailer: `[original][signature JSON][8-byte length][8-byte magic "MAJIKSIG"]` |
617
966
 
618
- All handlers guarantee: files remain fully usable after signing (PDFs still open, videos still play, Office files stay editable), signing is idempotent (safe to re-sign), and `strip()` always reproduces exactly the bytes that were originally hashed — including deterministic ZIP re-canonicalization for Office formats, so that identical content always strips to identical bytes regardless of when it was last re-zipped.
967
+ All handlers guarantee: files remain fully usable after signing (PDFs still open, videos still play, Office files stay editable), signing is idempotent (safe to re-sign), and `strip()` always reproduces exactly the bytes that were originally hashed — including deterministic ZIP re-canonicalization for Office formats (the ZIP is always rebuilt via a full unzip/rezip pass on `strip()`, even when no signature entry exists yet), so that identical content always strips to identical bytes regardless of when it was last re-zipped.
968
+
969
+ The MP4/MOV handler additionally preserves box order and byte-for-byte trailing/leftover data at every nesting level (top-level boxes, `moov` children, and `udta` children) rather than silently discarding anything it doesn't recognize — unparsed trailing bytes are always carried forward rather than dropped.
619
970
 
620
971
  ---
621
972
 
@@ -641,7 +992,7 @@ A single signer's envelope (`MajikSignatureJSON`):
641
992
 
642
993
  `allowlistHash` and `tsa` are only present when applicable — omitted entirely otherwise, never `null`.
643
994
 
644
- What's actually embedded into a file is a **`MultiSigEnvelope`**, wrapping one or more of the above:
995
+ What's actually embedded into a file (or produced detached) is a **`MultiSigEnvelope`**, wrapping one or more of the above — modeled at runtime by `MajikSignatureEnvelope`:
645
996
 
646
997
  ```json
647
998
  {
@@ -651,12 +1002,31 @@ What's actually embedded into a file is a **`MultiSigEnvelope`**, wrapping one o
651
1002
  "allowlistSignerId": "fingerprint-of-issuer",
652
1003
  "sealHash": "128-hex-char-sha3-512-hash",
653
1004
  "sealTimestamp": "2026-01-01T00:00:00.000Z",
654
- "sealedBy": "fingerprint-of-issuer"
1005
+ "sealedBy": "fingerprint-of-issuer",
1006
+ "chainAnchors": [ /* MajikChainAnchor[], optional */ ]
655
1007
  }
656
1008
  ```
657
1009
 
658
1010
  Files signed before multi-sig support existed contain a bare `MajikSignatureJSON` object at the root instead of this wrapper. The library detects and promotes this shape transparently — every public API always returns/accepts `MultiSigEnvelope` semantics, and old signatures continue to verify unmodified.
659
1011
 
1012
+ A batch manifest (`MjksMapJSON`, backing `MajikSignatureMap`) is a flat list of per-path entries, each carrying its own detached envelope:
1013
+
1014
+ ```json
1015
+ {
1016
+ "version": 1,
1017
+ "createdAt": "2026-01-01T00:00:00.000Z",
1018
+ "entries": [
1019
+ {
1020
+ "path": "docs/report.pdf",
1021
+ "contentHash": "base64-sha256-of-original-content",
1022
+ "size": 245678,
1023
+ "mimeType": "application/pdf",
1024
+ "envelope": { "...": "MultiSigEnvelope for this file" }
1025
+ }
1026
+ ]
1027
+ }
1028
+ ```
1029
+
660
1030
  **Approximate serialized sizes (per signer):**
661
1031
 
662
1032
  | Format | Size |
@@ -668,19 +1038,54 @@ The dominant contributor is `mlDsaSignature` (~6 KB base64) and `signerMlDsaPubl
668
1038
 
669
1039
  ---
670
1040
 
1041
+ ## Binary Container Formats (MJKSIG / MJKSMAP)
1042
+
1043
+ Two dedicated, versioned, self-identifying binary containers exist for out-of-band travel and on-disk storage — distinct from `serialize()`/`deserialize()` (plain base64 of the JSON, no header), which remains for lightweight in-app round-tripping.
1044
+
1045
+ Both share the same header layout: `[magic bytes][1-byte version][1-byte reserved][4-byte big-endian payload length][payload JSON]`.
1046
+
1047
+ | Format | Magic | Magic Length | Header Length | Media Type | Extension |
1048
+ | ------ | ----- | ------------ | -------------- | ---------- | --------- |
1049
+ | `.mjksig` — a single detached envelope | `MJKSIG` | 6 bytes | 12 bytes | `application/vnd.majikah.mjksig` | `.mjksig` |
1050
+ | `.mjksmap` — a batch manifest | `MJKSMAP` | 7 bytes | 13 bytes | `application/vnd.majikah.mjksmap` | `.mjksmap` |
1051
+
1052
+ Both formats validate magic bytes, supported version, and declared payload length **before** attempting to parse the JSON payload — a truncated or corrupted buffer fails fast with a clear `MajikSignatureSerializationError` rather than an obscure `JSON.parse` error.
1053
+
1054
+ ```typescript
1055
+ // MJKSIG
1056
+ const bytes = envelope.toMJKSIGBytes(); // sync Uint8Array — Node scripts, direct fs writes
1057
+ const blob = envelope.toMJKSIG(); // Blob — browser downloads, zip packaging
1058
+ const restored = await MajikSignatureEnvelope.fromMJKSIG(blob); // accepts Blob or Uint8Array
1059
+ const isMjksig = await MajikSignatureEnvelope.isMJKSIG(blob); // cheap magic-byte sniff, no parse
1060
+ const version = await MajikSignatureEnvelope.getMJKSIGVersion(blob);
1061
+
1062
+ // MJKSMAP
1063
+ const mapBytes = map.toMJKSMAPBytes();
1064
+ const mapBlob = map.toMJKSMAP();
1065
+ const restoredMap = await MajikSignatureMap.fromMJKSMAP(mapBlob);
1066
+ const isMjksmap = await MajikSignatureMap.isMJKSMAP(mapBlob);
1067
+ ```
1068
+
1069
+ `MajikSignatureEnvelope.from(input)` and `MajikSignatureMap.from(input)` are universal entry points that accept an instance, its plain JSON shape, raw bytes, or a Blob — every method that accepts a detached envelope or map (`verifyFileDetached()`, `signFileDetached()`'s `existingEnvelope` option, `verifyFilesFromMjksMap()`, etc.) normalizes through these internally, so callers don't need to know or care which shape they currently have on hand.
1070
+
1071
+ ---
1072
+
671
1073
  ## Error Handling
672
1074
 
673
1075
  Majik Signature throws a typed error hierarchy rather than generic `Error` objects, so you can catch precisely what you need:
674
1076
 
675
1077
  | Error Class | Thrown when... |
676
1078
  | ------------------------------------- | ------------------------------------------------------------------------- |
677
- | `MajikSignatureError` | Base class; also thrown for general/unexpected failures |
1079
+ | `MajikSignatureError` | Base class; also thrown for general/unexpected failures (e.g. signing a sealed envelope, missing envelope on seal/anchor)|
678
1080
  | `MajikSignatureKeyError` | The key is locked, lacks signing keys, or isn't the required issuer |
679
- | `MajikSignatureVerificationError` | Verification fails unexpectedly (not the same as `valid: false`) |
680
- | `MajikSignatureSerializationError` | JSON/base64 parsing or encoding fails |
1081
+ | `MajikSignatureVerificationError` | Verification fails unexpectedly (not the same as `valid: false`), including a failed TSA signature check |
1082
+ | `MajikSignatureSerializationError` | JSON/base64/MJKSIG/MJKSMAP parsing or encoding fails, including malformed binary headers |
681
1083
  | `MajikSignatureAllowlistError` | A non-listed signer attempts to sign a restricted file |
1084
+ | `MajikSignatureValidationError` | A structural shape check fails — malformed envelope, malformed batch manifest entry, empty/duplicate batch paths, empty allowlist, mismatched seal fields, invalid `expectedOrder` input, etc. |
1085
+
1086
+ Note the distinction: `verify()`/`verifyFile()` return `{ valid: false, reason }` for a signature that *fails cryptographic verification* — that's an expected, handled outcome, not an exception. Exceptions are reserved for misuse (locked keys, malformed envelopes, disallowed signers, sealed files, malformed batch input, etc.).
682
1087
 
683
- Note the distinction: `verify()`/`verifyFile()` return `{ valid: false, reason }` for a signature that *fails cryptographic verification* — that's an expected, handled outcome, not an exception. Exceptions are reserved for misuse (locked keys, malformed envelopes, disallowed signers, sealed files, etc.).
1088
+ Order and batch verification methods follow the same philosophy: `SignatureOrderResult` and `FileVerifyResult` both report failure states (`pendingSigners`, `invalidSigners`, `violations`, `"tampered"`, `"not_in_map"`, etc.) as normal return values rather than throwing, since "this didn't pass" is an expected outcome you'll want to render in a UI — not an exceptional code path.
684
1089
 
685
1090
  ---
686
1091
 
@@ -697,6 +1102,9 @@ Note the distinction: `verify()`/`verifyFile()` return `{ valid: false, reason }
697
1102
  - **Allowlist integrity** — tampering with a restricted file's allowlist invalidates the issuer's own signature
698
1103
  - **Seal integrity** — sealed envelopes reject all further signing attempts, including from the issuer
699
1104
  - **Embed integrity** — file embedding always signs original bytes; the container format is never part of what's signed
1105
+ - **Detachment integrity** — detached verification always strips the target file first, so a stray embedded envelope never interferes with verifying against a separately-supplied envelope
1106
+ - **Batch relocation resistance** — `MajikSignatureMap` resolves files by content hash when a path lookup misses, so renaming/moving a signed batch after the fact doesn't break verification
1107
+ - **Order-check independence** — signing-order verification checks each expected signer against the public keys **you supplied**, not the keys self-asserted inside their own envelope entry, so a forged identity claim can't also forge its way into a valid order result
700
1108
 
701
1109
  ### What is Your Responsibility
702
1110
 
@@ -704,6 +1112,8 @@ Note the distinction: `verify()`/`verifyFile()` return `{ valid: false, reason }
704
1112
  - **Byte-for-byte content consistency** — the same bytes must be passed to both `sign()` and `verify()`. For strings, both sides must use UTF-8; for JSON, both sides must use the same `JSON.stringify()` output.
705
1113
  - **Key upgrade** — legacy `MajikKey` accounts without signing keys must be re-imported via `importFromMnemonicBackup()` before signing. Check with `key.hasSigningKeys`.
706
1114
  - **TSA trust** — the library verifies a TSA signature cryptographically, but trusting *which* TSA identity to accept is your application's decision.
1115
+ - **Timestamp trust level in order verification** — a self-reported (non-TSA) timestamp is tamper-evident but not independently attested; a signer could set their local clock to anything. Always check `result.usesUnattestedTimestamp` before treating an order result as strong proof rather than a claim.
1116
+ - **Chain anchor submission and confirmation** — this SDK never talks to a blockchain itself. Submitting the memo from `buildChainAnchorMemo()` and confirming the transaction is entirely your (or a companion product's) responsibility before calling `registerChainAnchor()`.
707
1117
 
708
1118
  ### What NOT to Do
709
1119
 
@@ -714,16 +1124,20 @@ Note the distinction: `verify()`/`verifyFile()` return `{ valid: false, reason }
714
1124
  - ❌ **DON'T** use `contentType` as a security mechanism — it is advisory only and not enforced
715
1125
  - ❌ **DON'T** assume a Tier-2 trailer signature survives re-muxing or re-encoding — use native-metadata formats where durability matters
716
1126
  - ❌ **DON'T** treat the experimental image-stamping APIs as stable in production
1127
+ - ❌ **DON'T** treat a non-strict `verifyFileOrder()` pass as proof that *no one else* signed — use `strict: true` when the complete signer set matters, not just the relative order of the signers you named
1128
+ - ❌ **DON'T** register a chain anchor before the corresponding transaction is actually confirmed externally — `registerChainAnchor()` trusts the `MajikChainAnchor` you pass it and does not itself verify on-chain state
717
1129
 
718
1130
  ### What TO Do
719
1131
 
720
1132
  - ✅ **DO** verify `result.signerId` for every entry returned by `verifyFile()` against a known trusted fingerprint
721
1133
  - ✅ **DO** use `verifyWithKey()` / `verifyFile(key)` when you have the signer's `MajikKey` — it handles key extraction safely
722
1134
  - ✅ **DO** lock the key immediately after signing — `key.lock()` purges secret keys from memory
723
- - ✅ **DO** use `signFile()` for media and documents to keep signature and content together
1135
+ - ✅ **DO** use `signFile()` for media and documents to keep signature and content together, or `signFileDetached()` when the envelope needs to live and travel separately from the file
724
1136
  - ✅ **DO** use `isSigned()` as a fast guard before calling `verifyFile()` in hot paths
725
1137
  - ✅ **DO** use `canSign()` to give users a clear reason *before* they attempt to sign a restricted file
726
1138
  - ✅ **DO** use `CONTENT_TYPES` constants for standard content type labels
1139
+ - ✅ **DO** use `signBatchDetached()` with `continueOnError: true` for large batches where a handful of unreadable files shouldn't block the rest — and inspect `failures` afterward
1140
+ - ✅ **DO** check `result.softTieWarnings` even on a passing order-verification result — it's a legitimate caveat worth surfacing, not just a failure signal
727
1141
 
728
1142
  ---
729
1143
 
@@ -735,7 +1149,6 @@ Majik Signature is the cryptographic signing layer shared across Majikah's produ
735
1149
 
736
1150
  [![Majik Signature Hero](https://github.com/user-attachments/assets/781bb778-9535-4b1f-bbc5-820550ecc864)](https://signature.majikah.solutions)
737
1151
 
738
-
739
1152
  The standalone desktop and web application built on top of this SDK. It's the fastest way to sign and verify files without writing any code:
740
1153
 
741
1154
  - Sign virtually any file — documents, PDFs, Office files, images, audio, video, source code, archives, and more — entirely **locally on your device**. No account, no upload, no internet connection required for offline signing and verification.
@@ -743,7 +1156,7 @@ The standalone desktop and web application built on top of this SDK. It's the fa
743
1156
  - **Audio Stamping** — embed producer tags, voice tags, or audio watermarks directly into signed audio, with a full mixing timeline (volume, pan, pitch, EQ, trim, loop).
744
1157
  - **Trusted Timestamps** — every account gets 5 free Trusted Timestamps every 24 hours; local timestamps remain fully supported offline.
745
1158
  - **Batch processing** — drag-and-drop folders, recursive ZIP processing, individual or bulk sealing, per-file verification results.
746
- - **Multi-party workflows** — signing allowlists, open or restricted modes, progress tracking for pending vs. completed signatures.
1159
+ - **Multi-party workflows** — signing allowlists, open or restricted modes, progress tracking for pending vs. completed signatures, and chronological signing-order verification.
747
1160
  - Built with Tauri for a lightweight, fast, secure desktop experience — available on the **Microsoft Store**, with a full-featured **web app** as well.
748
1161
 
749
1162
  ### 🧾 Majik Buwiz
@@ -788,7 +1201,6 @@ If you want to contribute or help extend support to more platforms or file forma
788
1201
 
789
1202
  Developed by **Josef Elijah Fabian (Zelijah)** | [Majikah Solutions OPC](https://majikah.solutions/about)
790
1203
 
791
-
792
1204
  **Developer**: [Josef Elijah Fabian](https://github.com/jedlsf)
793
1205
  **GitHub**: [https://github.com/Majikah](https://github.com/Majikah)
794
1206
  **Project Repository**: [https://github.com/Majikah/majik-signature](https://github.com/Majikah/majik-signature)