@majikah/majik-signature 0.2.7 → 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.
package/README.md CHANGED
@@ -5,7 +5,7 @@
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,38 @@ 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
+ - **Detached envelopes** — sign a file and receive the envelope separately, for external verification pipelines where payload and signature travel independently
200
+ - **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
201
+ - **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
202
+ - **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
203
+ - **Per-file batch verification reporting** — `verified` / `invalid` / `tampered` / `not_in_map` status per file, plus a one-glance summary
204
+
191
205
  ### Content Format Support
192
206
 
193
207
  - **Plain text, JSON, binary** — `Uint8Array` or `string`
194
208
  - **PDF** — signature appended as a spec-compliant binary trailer after the file's `%%EOF`
195
- - **PNG** — embedded in a `iTXt` metadata chunk
209
+ - **PNG** — embedded in an `iTXt` metadata chunk
196
210
  - **WAV** — embedded in a RIFF `LIST INFO` chunk
197
211
  - **MP3** — embedded in an ID3v2 `TXXX` frame
198
- - **MP4, MOV, M4A, M4V** — embedded in the `moov/udta` box
212
+ - **MP4, MOV, M4A, M4V** — embedded in the `moov/udta` box, custom `majk` box type
199
213
  - **DOCX, XLSX, PPTX, ODT, ODS, ODP** — embedded as a dedicated entry inside the ZIP container
200
214
  - **MKV, WebM** — embedded via a custom Matroska metadata tag
215
+ - **JPEG, FLAC** — native format metadata, same round-trip guarantees as other handlers
201
216
  - **HTML, Markdown, JSON, plain text, source code** — embedded as an appended, format-appropriate metadata block
202
217
  - **Any other format** — universal binary trailer: `[original bytes][signature JSON][8-byte length][8-byte magic]` — cleanly detectable and strippable regardless of format
203
218
 
@@ -206,19 +221,21 @@ See [Supported File Formats](#supported-file-formats) for the full handler table
206
221
  ### Developer Experience
207
222
 
208
223
  - **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
224
+ - **Simple core API** — `sign()` / `verify()` for bytes and strings; `signFile()` / `verifyFile()` for embedded files; `signFileDetached()` / `verifyFileDetached()` for detached workflows
210
225
  - **One-liner file signing** — `MajikSignature.signFile(blob, key)` signs and embeds in a single call
211
226
  - **Format auto-detection** — MIME type and magic-byte sniffing, no manual format hints required in most cases
212
227
  - **Idempotent re-signing** — safely re-sign or re-embed any file without accumulating stacked or orphaned envelopes
228
+ - **Immutable envelope model** — `MajikSignatureEnvelope` and `MajikSignatureMap` are both immutable; every mutation returns a new instance
213
229
  - **Typed error hierarchy** — precise, catchable error classes instead of generic exceptions
214
230
  - **Isomorphic** — Node.js, browsers, Tauri, Deno, and Bun; no native bindings
215
231
 
216
232
  ### Serialization & Portability
217
233
 
218
- - **JSON envelope** — full `toJSON()` / `fromJSON()` round-trip
234
+ - **JSON envelope** — full `toJSON()` / `fromJSON()` round-trip, at both the signature and envelope level
219
235
  - **Base64 serialization** — `serialize()` / `deserialize()` for compact transport (HTTP headers, DB columns, etc.)
220
236
  - **File-embedded** — the signature lives inside the file itself, no sidecar files needed
221
237
  - **Self-contained** — the envelope includes the signer's public keys, verifiable without a separate key registry
238
+ - **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
239
 
223
240
  ---
224
241
 
@@ -303,6 +320,42 @@ const originalBlob = await MajikSignature.stripFrom(signedBlob);
303
320
 
304
321
  ---
305
322
 
323
+ ### Detached Signing
324
+
325
+ 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.
326
+
327
+ ```typescript
328
+ import { MajikSignature } from '@majikah/majik-signature';
329
+
330
+ // Sign — returns the clean file bytes AND the envelope, separately
331
+ const { blob, envelope, signature } = await MajikSignature.signFileDetached(file, aliceKey);
332
+
333
+ // The envelope can be stored/transported however you like:
334
+ const envelopeJson = envelope.toJSON();
335
+ const envelopeB64 = envelope.serialize();
336
+ const envelopeBinary = envelope.toMJKSIG(); // a portable .mjksig Blob
337
+
338
+ // Verify later, against the same clean file bytes + the detached envelope
339
+ const results = await MajikSignature.verifyFileDetached(blob, envelope, aliceKey);
340
+ console.log('Valid:', results.every((r) => r.valid));
341
+
342
+ // Multiple signers can be added to the same detached envelope before it's ever embedded
343
+ const { envelope: envelope2 } = await MajikSignature.signFileDetached(blob, bobKey, {
344
+ existingEnvelope: envelope, // continue from Alice's envelope
345
+ });
346
+ ```
347
+
348
+ You can attach a Trusted Timestamp to a detached signature in the same call:
349
+
350
+ ```typescript
351
+ const { envelope, signature } = await MajikSignature.signFileDetached(file, aliceKey, {
352
+ tsa: myTsaTimestamp,
353
+ });
354
+ console.log(signature.hasTSA); // true
355
+ ```
356
+
357
+ ---
358
+
306
359
  ### Multi-Signature Files & Allowlists
307
360
 
308
361
  ```typescript
@@ -385,6 +438,111 @@ console.log('TSA still valid:', signature.verifyTSA().valid);
385
438
 
386
439
  ---
387
440
 
441
+ ### Batch Signing a Folder
442
+
443
+ Sign every file in a folder, project export, or zip's contents in one call, packaged as a single manifest.
444
+
445
+ ```typescript
446
+ import { MajikSignature } from '@majikah/majik-signature';
447
+
448
+ const result = await MajikSignature.signBatchDetached(
449
+ [
450
+ { path: 'docs/report.pdf', blob: reportBlob },
451
+ { path: 'docs/appendix.pdf', blob: appendixBlob },
452
+ { path: 'assets/cover.png', blob: coverBlob },
453
+ ],
454
+ aliceKey,
455
+ );
456
+
457
+ if (result.mode === 'map') {
458
+ // result.map is a MajikSignatureMap instance; result.mapBlob is a ready-to-store .mjksmap Blob
459
+ zip.file('signatures.mjksmap', await result.mapBlob.arrayBuffer());
460
+ }
461
+
462
+ // Later — verify an extracted batch against the manifest
463
+ const map = await MajikSignature.getSignatureMapFromMJKSMAP(mapBlob);
464
+ // (or: const map = await MajikSignatureMap.fromMJKSMAP(mapBlob);)
465
+
466
+ const results = await MajikSignature.verifyFilesFromMjksMap(
467
+ map,
468
+ extractedFiles, // { path, blob }[]
469
+ publicKeys,
470
+ );
471
+
472
+ const summary = MajikSignature.summarizeBatchVerification(results);
473
+ console.log(`${summary.verified}/${summary.total} verified`);
474
+
475
+ if (!summary.allValid) {
476
+ for (const r of results) {
477
+ if (r.status !== 'verified') console.log(r.path, r.status, r.reason);
478
+ }
479
+ }
480
+ ```
481
+
482
+ 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.
483
+
484
+ ---
485
+
486
+ ### Verifying Signing Order
487
+
488
+ 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.
489
+
490
+ ```typescript
491
+ import { MajikSignature } from '@majikah/majik-signature';
492
+
493
+ // expectedOrder accepts MajikKey instances and/or ExpectedSigner objects, mixed freely
494
+ const result = await MajikSignature.verifyFileOrder(signedBlob, [bobKey, daveKey]);
495
+
496
+ if (!result.allExpectedSigned) {
497
+ console.log('Still pending:', result.pendingSigners);
498
+ } else if (!result.allValid) {
499
+ console.log('Invalid/tampered signature(s):', result.invalidSigners);
500
+ } else if (!result.orderRespected) {
501
+ console.log('Signed out of order:', result.violations);
502
+ } else if (result.softTieWarnings.length > 0) {
503
+ console.log('Valid, but note:', result.reason); // identical timestamps — order indistinguishable
504
+ } else {
505
+ console.log('Signed in the expected order — all valid.');
506
+ }
507
+
508
+ if (result.usesUnattestedTimestamp) {
509
+ console.log('Note: order relied on at least one self-reported (non-TSA) timestamp.');
510
+ }
511
+
512
+ // Strict mode — fail if anyone outside expectedOrder signed at all
513
+ const strictResult = await MajikSignature.verifyFileOrder(
514
+ signedBlob,
515
+ [bobKey, daveKey],
516
+ { strict: true },
517
+ );
518
+ ```
519
+
520
+ The same check works against a detached envelope via `verifyFileDetachedOrder(file, envelope, expectedOrder, options?)`.
521
+
522
+ ---
523
+
524
+ ### Chain Anchoring
525
+
526
+ 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.
527
+
528
+ ```typescript
529
+ import { MajikSignature } from '@majikah/majik-signature';
530
+
531
+ // 1. File must be sealed first
532
+ const { permitted, reason } = await MajikSignature.canAnchor(sealedBlob);
533
+
534
+ // 2. Build the canonical memo to submit on-chain (elsewhere, e.g. via majik-notary)
535
+ const memo = MajikSignature.buildChainAnchorMemo(sealInfo.sealHash);
536
+
537
+ // 3. Once the external chain integration confirms the transaction, register the anchor
538
+ const blob = await MajikSignature.registerChainAnchor(sealedBlob, confirmedAnchor);
539
+
540
+ // 4. Read anchors back later
541
+ const anchors = await MajikSignature.getChainAnchors(blob);
542
+ ```
543
+
544
+ ---
545
+
388
546
  ## API Reference
389
547
 
390
548
  ### Content Signing (bytes/strings)
@@ -398,6 +556,7 @@ Sign raw bytes or a string with an unlocked `MajikKey`.
398
556
  - `options?: SignOptions`
399
557
  - `contentType?: string` — advisory label (see `CONTENT_TYPES`)
400
558
  - `timestamp?: string` — ISO 8601 override (defaults to `new Date().toISOString()`)
559
+ - `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
560
 
402
561
  **Returns:** `Promise<MajikSignature>`
403
562
  **Throws:** `MajikSignatureKeyError` if the key is locked or has no signing keys.
@@ -417,7 +576,8 @@ Verify a signature against content and the signer's public keys. Both Ed25519 an
417
576
  contentHash?: string;
418
577
  timestamp: string;
419
578
  contentType?: string;
420
- reason?: string; // present when valid is false
579
+ handler?: string; // present when result came from a file-level verify
580
+ reason?: string; // present when valid is false
421
581
  }
422
582
  ```
423
583
 
@@ -429,6 +589,14 @@ Convenience wrapper — verifies directly against a `MajikKey` instance. Works e
429
589
  #### `MajikSignature.publicKeysFromMajikKey(key)`
430
590
  Extracts `{ signerId, edPublicKey, mlDsaPublicKey }` from a `MajikKey` for use with `verify()`. Works on locked keys.
431
591
 
592
+ #### `signature.extractPublicKeys()` *(instance method)*
593
+ 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.
594
+
595
+ > ⚠️ 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).
596
+
597
+ #### `signature.validate()` / `signature.isValid()`
598
+ `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.
599
+
432
600
  #### `MajikSignature.fromJSON(json)` / `MajikSignature.deserialize(base64)`
433
601
  Reconstruct a `MajikSignature` instance from stored JSON or a base64 string.
434
602
 
@@ -450,7 +618,7 @@ Sign a file and embed the signature in one call. Strips any existing envelope fi
450
618
  - `mimeType?: string` — override auto-detected MIME type
451
619
  - `expectedSigners?: ExpectedSigner[]` — only honored on the **first** signature; establishes a signing allowlist
452
620
 
453
- **Returns:** `Promise<{ blob: Blob; signature: MajikSignature; handler: string; mimeType: string }>`
621
+ **Returns:** `Promise<{ blob: Blob; signature: MajikSignature; envelope: MajikSignatureEnvelope; handler: string; mimeType: string }>`
454
622
 
455
623
  ---
456
624
 
@@ -487,10 +655,55 @@ Structural presence check — does the file contain an envelope at all? Does not
487
655
 
488
656
  ---
489
657
 
658
+ ### Detached Signing & MajikSignatureEnvelope API
659
+
660
+ #### `MajikSignature.signFileDetached(file, key, options?)`
661
+
662
+ Sign a file and return the resulting envelope **detached** — the returned `blob` is the clean, stripped file; the envelope travels separately.
663
+
664
+ - `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)
665
+ - `options?.expectedSigners?: ExpectedSigner[]` — same allowlist-establishing semantics as `signFile()`
666
+ - `options?.tsa?: MajikTimestamp` — attach a Trusted Timestamp to this signer's entry before it's added to the envelope
667
+
668
+ **Returns:** `Promise<{ blob: Blob; envelope: MajikSignatureEnvelope; signature: MajikSignature; handler: string; mimeType: string }>`
669
+
670
+ #### `MajikSignature.verifyFileDetached(file, envelope, keyOrPublicKeys, options?)`
671
+ 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.
672
+
673
+ **Returns:** `Promise<VerificationResult[]>`
674
+
675
+ ---
676
+
677
+ #### `MajikSignatureEnvelope`
678
+
679
+ 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.
680
+
681
+ **State predicates:** `isSealed()`, `hasAllowlist()`, `isFirstSigner()`, `isMultiSig()`, `hasMultipleSignatories()`, `isIssuer(keyOrFingerprint)`
682
+
683
+ **Lookup:** `findSignature(signerId)`, `get signatures`, `get allowlist`, `get chainAnchors`
684
+
685
+ **Allowlist enforcement:** `checkAllowlist(key)`, `assertCanSign(key)` (throws), `canSign(key)` (non-throwing), `verifyAllowlistIntegrity()`
686
+
687
+ **Builders (immutable):** `withSignature(sig)`, `withAllowlist(allowlist, signerId)`, `withSeal(sealedBy, timestamp?)`, `withChainAnchor(anchor)`
688
+
689
+ **Seal queries:** `verifySeal()`, `getSealInfo()`, `canAnchor()`
690
+
691
+ **Signatory resolution:** `resolveIssuer()`, `getSignatories(filter?)`, `getEnvelopeInfo()`
692
+
693
+ **Serialization:** `toJSON()`, `serialize()` / `MajikSignatureEnvelope.deserialize(base64)`, `toMJKSIG()` / `toMJKSIGBytes()` / `MajikSignatureEnvelope.fromMJKSIG(input)`, `MajikSignatureEnvelope.isMJKSIG(input)`, `MajikSignatureEnvelope.getMJKSIGVersion(input)`
694
+
695
+ **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)
696
+
697
+ **Validation:** `validate()` (throws), `isValid()` (boolean)
698
+
699
+ See [Binary Container Formats](#binary-container-formats-mjksig--mjksmap) for details on `.mjksig`.
700
+
701
+ ---
702
+
490
703
  ### Multi-Signature & Allowlist API
491
704
 
492
705
  #### `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.
706
+ 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
707
 
495
708
  #### `MajikSignature.getAllowlist(file, options?)`
496
709
  **Returns:** `Promise<ExpectedSigner[] | null>` — `null` for open-signing or unsigned files.
@@ -557,7 +770,7 @@ Builds a `MajikTSARequest` (`{ digest: { algorithm: "SHA-256", value } }`) from
557
770
  **Server-side.** Signs a TSA payload and returns a complete `MajikTimestamp`, including a server-generated nonce and timestamp.
558
771
 
559
772
  #### `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.
773
+ 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
774
 
562
775
  #### `signature.verifyTSA()`
563
776
  Re-verifies the attached TSA's own signature — useful after deserializing a signature from storage.
@@ -567,6 +780,131 @@ Re-verifies the attached TSA's own signature — useful after deserializing a si
567
780
 
568
781
  ---
569
782
 
783
+ ### Batch Signing & MajikSignatureMap API
784
+
785
+ #### `MajikSignature.signBatchDetached(files, key, options?)`
786
+
787
+ Sign a batch of files (e.g. a folder or zip's contents) as detached envelopes.
788
+
789
+ - `files: { path: string; blob: Blob }[]` — every path must be unique within the batch
790
+ - `options?.mode?: "map" | "separate"` — `"map"` (default) produces one `MajikSignatureMap` covering the whole batch; `"separate"` produces one `.mjksig` Blob per file
791
+ - `options?.continueOnError?: boolean` — default `false` (abort the whole batch on the first failure); set `true` to collect failures per-file and continue
792
+
793
+ **Returns:**
794
+ ```typescript
795
+ | { mode: "map"; map: MajikSignatureMap; mapBlob: Blob; failures: BatchSignFailure[] }
796
+ | { mode: "separate"; signatures: { path: string; blob: Blob }[]; failures: BatchSignFailure[] }
797
+ ```
798
+
799
+ #### `MajikSignature.verifyFilesFromMjksMap(map, files, publicKeys, options?)`
800
+ 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.
801
+
802
+ - `options?.expectedSignerId?: string`
803
+ - `options?.requireAllPresent?: boolean` — escalate a missing file to a thrown error instead of a per-file `"not_in_map"` result
804
+
805
+ **Returns:** `Promise<FileVerifyResult[]>`
806
+
807
+ ```typescript
808
+ {
809
+ path: string;
810
+ status: "verified" | "invalid" | "tampered" | "not_in_map";
811
+ results?: VerificationResult[];
812
+ reason?: string;
813
+ relocatedFrom?: string; // present only when found by content match at a different path
814
+ }
815
+ ```
816
+
817
+ #### `MajikSignature.verifyFilesFromMjksMapWithKey(map, files, key, options?)`
818
+ Convenience overload — resolves public keys from a `MajikKey` instead of requiring `MajikSignerPublicKeys` directly.
819
+
820
+ #### `MajikSignature.summarizeBatchVerification(results)`
821
+ One-glance pass/fail summary over a `FileVerifyResult[]`.
822
+
823
+ **Returns:** `{ total, verified, invalid, tampered, notInMap, allValid }`
824
+
825
+ ---
826
+
827
+ #### `MajikSignatureMap`
828
+
829
+ 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.
830
+
831
+ **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()`
832
+
833
+ **Builders (immutable):** `withEntry(entry)`, `withoutEntry(path)`
834
+
835
+ **Serialization:** `toJSON()`, `toMJKSMAP()` / `toMJKSMAPBytes()` / `MajikSignatureMap.fromMJKSMAP(input)`, `MajikSignatureMap.isMJKSMAP(input)`
836
+
837
+ **Creation / parsing:** `MajikSignatureMap.empty()`, `MajikSignatureMap.fromJSON(json)`, `MajikSignatureMap.from(input)` (accepts an instance, JSON, `.mjksmap` bytes, or Blob)
838
+
839
+ **Validation:** `validate()` (throws), `isValid()` (boolean)
840
+
841
+ `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"`.
842
+
843
+ See [Binary Container Formats](#binary-container-formats-mjksig--mjksmap) for details on `.mjksmap`.
844
+
845
+ ---
846
+
847
+ ### Signature Order Verification API
848
+
849
+ 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.
850
+
851
+ #### `MajikSignature.verifyFileOrder(file, expectedOrder, options?)`
852
+
853
+ - `expectedOrder: (MajikKey | ExpectedSigner)[]` — array position is the expected chronological position; `MajikKey` instances and `ExpectedSigner` objects can be mixed freely and are normalized internally
854
+ - `options?.strict?: boolean` — default `false`. When `true`, any signer present in the envelope but absent from `expectedOrder` fails the overall result
855
+
856
+ **Returns:** `Promise<SignatureOrderResult>`
857
+
858
+ ```typescript
859
+ {
860
+ valid: boolean; // true only if everyone expected signed, all valid, order respected (and, in strict mode, no extra signers)
861
+ allExpectedSigned: boolean;
862
+ allValid: boolean;
863
+ orderRespected: boolean;
864
+ strict: boolean;
865
+ unexpectedSigners: string[]; // populated only when strict is true
866
+ pendingSigners: string[];
867
+ invalidSigners: string[];
868
+ violations: OrderViolation[]; // { earlier, later, earlierTimestamp, laterTimestamp }
869
+ usesUnattestedTimestamp: boolean;
870
+ softTieWarnings: SoftTieWarning[]; // identical timestamps between two signers — order indistinguishable, doesn't fail `valid`
871
+ signers: SignerOrderStatus[];
872
+ reason?: string;
873
+ }
874
+ ```
875
+
876
+ #### `MajikSignature.verifyFileDetachedOrder(file, envelope, expectedOrder, options?)`
877
+ Same semantics as `verifyFileOrder()`, against a detached envelope (instance, JSON, `.mjksig` bytes, or Blob).
878
+
879
+ #### `MajikSignature.normalizeExpectedOrder(expectedOrder)`
880
+ 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.
881
+
882
+ > 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.
883
+
884
+ ---
885
+
886
+ ### Chain Anchoring API (Experimental)
887
+
888
+ > ⚠️ **These APIs are explicitly marked experimental in source and are not yet API-stable.** Expect breaking changes across minor versions.
889
+
890
+ #### `MajikSignature.canAnchor(file, options?)`
891
+ Checks whether a file is eligible for chain anchoring (requires the envelope to be sealed).
892
+
893
+ **Returns:** `Promise<{ permitted: boolean; reason?: string }>`
894
+
895
+ #### `MajikSignature.buildChainAnchorMemo(sealHash)`
896
+ 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.
897
+
898
+ #### `MajikSignature.registerChainAnchor(file, anchor, options?)`
899
+ 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.
900
+
901
+ **Returns:** `Promise<Blob>`
902
+
903
+ #### `MajikSignature.getChainAnchors(file, options?)`
904
+ **Returns:** `Promise<MajikChainAnchor[]>` — empty array if none, or if the file has no envelope.
905
+
906
+ ---
907
+
570
908
  ### Image Stamping (Experimental)
571
909
 
572
910
  > ⚠️ **These APIs are explicitly marked experimental in source and are not yet API-stable.** Expect breaking changes across minor versions.
@@ -583,6 +921,15 @@ Full round-trip to/from the `MajikSignatureJSON` shape.
583
921
  #### `signature.serialize()` / `MajikSignature.deserialize(base64)`
584
922
  Compact base64 transport format — useful for HTTP headers or database columns.
585
923
 
924
+ #### `envelope.toJSON()` / `MajikSignatureEnvelope.fromJSON(json)`
925
+ Full round-trip to/from the `MultiSigEnvelope` shape, including transparent promotion of legacy bare single-sig JSON.
926
+
927
+ #### `envelope.serialize()` / `MajikSignatureEnvelope.deserialize(base64)`
928
+ Base64 transport for a full multi-sig envelope.
929
+
930
+ #### `envelope.toMJKSIG()` / `MajikSignatureEnvelope.fromMJKSIG(input)`
931
+ Self-describing binary container for a detached envelope — see [Binary Container Formats](#binary-container-formats-mjksig--mjksmap).
932
+
586
933
  ```typescript
587
934
  const signature = await MajikSignature.sign(content, key);
588
935
 
@@ -615,7 +962,9 @@ Handlers are tried in order; the first one whose `canHandle()` matches wins. If
615
962
  | HTML, Markdown, JSON, plain text, source code | Text | Appended, format-appropriate metadata block |
616
963
  | Anything else | Fallback | Universal binary trailer: `[original][signature JSON][8-byte length][8-byte magic "MAJIKSIG"]` |
617
964
 
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.
965
+ 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.
966
+
967
+ 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
968
 
620
969
  ---
621
970
 
@@ -641,7 +990,7 @@ A single signer's envelope (`MajikSignatureJSON`):
641
990
 
642
991
  `allowlistHash` and `tsa` are only present when applicable — omitted entirely otherwise, never `null`.
643
992
 
644
- What's actually embedded into a file is a **`MultiSigEnvelope`**, wrapping one or more of the above:
993
+ 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
994
 
646
995
  ```json
647
996
  {
@@ -651,12 +1000,31 @@ What's actually embedded into a file is a **`MultiSigEnvelope`**, wrapping one o
651
1000
  "allowlistSignerId": "fingerprint-of-issuer",
652
1001
  "sealHash": "128-hex-char-sha3-512-hash",
653
1002
  "sealTimestamp": "2026-01-01T00:00:00.000Z",
654
- "sealedBy": "fingerprint-of-issuer"
1003
+ "sealedBy": "fingerprint-of-issuer",
1004
+ "chainAnchors": [ /* MajikChainAnchor[], optional */ ]
655
1005
  }
656
1006
  ```
657
1007
 
658
1008
  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
1009
 
1010
+ A batch manifest (`MjksMapJSON`, backing `MajikSignatureMap`) is a flat list of per-path entries, each carrying its own detached envelope:
1011
+
1012
+ ```json
1013
+ {
1014
+ "version": 1,
1015
+ "createdAt": "2026-01-01T00:00:00.000Z",
1016
+ "entries": [
1017
+ {
1018
+ "path": "docs/report.pdf",
1019
+ "contentHash": "base64-sha256-of-original-content",
1020
+ "size": 245678,
1021
+ "mimeType": "application/pdf",
1022
+ "envelope": { "...": "MultiSigEnvelope for this file" }
1023
+ }
1024
+ ]
1025
+ }
1026
+ ```
1027
+
660
1028
  **Approximate serialized sizes (per signer):**
661
1029
 
662
1030
  | Format | Size |
@@ -668,19 +1036,54 @@ The dominant contributor is `mlDsaSignature` (~6 KB base64) and `signerMlDsaPubl
668
1036
 
669
1037
  ---
670
1038
 
1039
+ ## Binary Container Formats (MJKSIG / MJKSMAP)
1040
+
1041
+ 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.
1042
+
1043
+ Both share the same header layout: `[magic bytes][1-byte version][1-byte reserved][4-byte big-endian payload length][payload JSON]`.
1044
+
1045
+ | Format | Magic | Magic Length | Header Length | Media Type | Extension |
1046
+ | ------ | ----- | ------------ | -------------- | ---------- | --------- |
1047
+ | `.mjksig` — a single detached envelope | `MJKSIG` | 6 bytes | 12 bytes | `application/vnd.majikah.mjksig` | `.mjksig` |
1048
+ | `.mjksmap` — a batch manifest | `MJKSMAP` | 7 bytes | 13 bytes | `application/vnd.majikah.mjksmap` | `.mjksmap` |
1049
+
1050
+ 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.
1051
+
1052
+ ```typescript
1053
+ // MJKSIG
1054
+ const bytes = envelope.toMJKSIGBytes(); // sync Uint8Array — Node scripts, direct fs writes
1055
+ const blob = envelope.toMJKSIG(); // Blob — browser downloads, zip packaging
1056
+ const restored = await MajikSignatureEnvelope.fromMJKSIG(blob); // accepts Blob or Uint8Array
1057
+ const isMjksig = await MajikSignatureEnvelope.isMJKSIG(blob); // cheap magic-byte sniff, no parse
1058
+ const version = await MajikSignatureEnvelope.getMJKSIGVersion(blob);
1059
+
1060
+ // MJKSMAP
1061
+ const mapBytes = map.toMJKSMAPBytes();
1062
+ const mapBlob = map.toMJKSMAP();
1063
+ const restoredMap = await MajikSignatureMap.fromMJKSMAP(mapBlob);
1064
+ const isMjksmap = await MajikSignatureMap.isMJKSMAP(mapBlob);
1065
+ ```
1066
+
1067
+ `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.
1068
+
1069
+ ---
1070
+
671
1071
  ## Error Handling
672
1072
 
673
1073
  Majik Signature throws a typed error hierarchy rather than generic `Error` objects, so you can catch precisely what you need:
674
1074
 
675
1075
  | Error Class | Thrown when... |
676
1076
  | ------------------------------------- | ------------------------------------------------------------------------- |
677
- | `MajikSignatureError` | Base class; also thrown for general/unexpected failures |
1077
+ | `MajikSignatureError` | Base class; also thrown for general/unexpected failures (e.g. signing a sealed envelope, missing envelope on seal/anchor)|
678
1078
  | `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 |
1079
+ | `MajikSignatureVerificationError` | Verification fails unexpectedly (not the same as `valid: false`), including a failed TSA signature check |
1080
+ | `MajikSignatureSerializationError` | JSON/base64/MJKSIG/MJKSMAP parsing or encoding fails, including malformed binary headers |
681
1081
  | `MajikSignatureAllowlistError` | A non-listed signer attempts to sign a restricted file |
1082
+ | `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. |
1083
+
1084
+ 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
1085
 
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.).
1086
+ 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
1087
 
685
1088
  ---
686
1089
 
@@ -697,6 +1100,9 @@ Note the distinction: `verify()`/`verifyFile()` return `{ valid: false, reason }
697
1100
  - **Allowlist integrity** — tampering with a restricted file's allowlist invalidates the issuer's own signature
698
1101
  - **Seal integrity** — sealed envelopes reject all further signing attempts, including from the issuer
699
1102
  - **Embed integrity** — file embedding always signs original bytes; the container format is never part of what's signed
1103
+ - **Detachment integrity** — detached verification always strips the target file first, so a stray embedded envelope never interferes with verifying against a separately-supplied envelope
1104
+ - **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
1105
+ - **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
1106
 
701
1107
  ### What is Your Responsibility
702
1108
 
@@ -704,6 +1110,8 @@ Note the distinction: `verify()`/`verifyFile()` return `{ valid: false, reason }
704
1110
  - **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
1111
  - **Key upgrade** — legacy `MajikKey` accounts without signing keys must be re-imported via `importFromMnemonicBackup()` before signing. Check with `key.hasSigningKeys`.
706
1112
  - **TSA trust** — the library verifies a TSA signature cryptographically, but trusting *which* TSA identity to accept is your application's decision.
1113
+ - **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.
1114
+ - **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
1115
 
708
1116
  ### What NOT to Do
709
1117
 
@@ -714,16 +1122,20 @@ Note the distinction: `verify()`/`verifyFile()` return `{ valid: false, reason }
714
1122
  - ❌ **DON'T** use `contentType` as a security mechanism — it is advisory only and not enforced
715
1123
  - ❌ **DON'T** assume a Tier-2 trailer signature survives re-muxing or re-encoding — use native-metadata formats where durability matters
716
1124
  - ❌ **DON'T** treat the experimental image-stamping APIs as stable in production
1125
+ - ❌ **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
1126
+ - ❌ **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
1127
 
718
1128
  ### What TO Do
719
1129
 
720
1130
  - ✅ **DO** verify `result.signerId` for every entry returned by `verifyFile()` against a known trusted fingerprint
721
1131
  - ✅ **DO** use `verifyWithKey()` / `verifyFile(key)` when you have the signer's `MajikKey` — it handles key extraction safely
722
1132
  - ✅ **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
1133
+ - ✅ **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
1134
  - ✅ **DO** use `isSigned()` as a fast guard before calling `verifyFile()` in hot paths
725
1135
  - ✅ **DO** use `canSign()` to give users a clear reason *before* they attempt to sign a restricted file
726
1136
  - ✅ **DO** use `CONTENT_TYPES` constants for standard content type labels
1137
+ - ✅ **DO** use `signBatchDetached()` with `continueOnError: true` for large batches where a handful of unreadable files shouldn't block the rest — and inspect `failures` afterward
1138
+ - ✅ **DO** check `result.softTieWarnings` even on a passing order-verification result — it's a legitimate caveat worth surfacing, not just a failure signal
727
1139
 
728
1140
  ---
729
1141
 
@@ -735,7 +1147,6 @@ Majik Signature is the cryptographic signing layer shared across Majikah's produ
735
1147
 
736
1148
  [![Majik Signature Hero](https://github.com/user-attachments/assets/781bb778-9535-4b1f-bbc5-820550ecc864)](https://signature.majikah.solutions)
737
1149
 
738
-
739
1150
  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
1151
 
741
1152
  - 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 +1154,7 @@ The standalone desktop and web application built on top of this SDK. It's the fa
743
1154
  - **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
1155
  - **Trusted Timestamps** — every account gets 5 free Trusted Timestamps every 24 hours; local timestamps remain fully supported offline.
745
1156
  - **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.
1157
+ - **Multi-party workflows** — signing allowlists, open or restricted modes, progress tracking for pending vs. completed signatures, and chronological signing-order verification.
747
1158
  - Built with Tauri for a lightweight, fast, secure desktop experience — available on the **Microsoft Store**, with a full-featured **web app** as well.
748
1159
 
749
1160
  ### 🧾 Majik Buwiz
@@ -788,7 +1199,6 @@ If you want to contribute or help extend support to more platforms or file forma
788
1199
 
789
1200
  Developed by **Josef Elijah Fabian (Zelijah)** | [Majikah Solutions OPC](https://majikah.solutions/about)
790
1201
 
791
-
792
1202
  **Developer**: [Josef Elijah Fabian](https://github.com/jedlsf)
793
1203
  **GitHub**: [https://github.com/Majikah](https://github.com/Majikah)
794
1204
  **Project Repository**: [https://github.com/Majikah/majik-signature](https://github.com/Majikah/majik-signature)
@@ -53,6 +53,26 @@ export interface MajikTSAPayload {
53
53
  signerFingerprint: string;
54
54
  };
55
55
  }
56
+ /**
57
+ * Wire-optimized signature envelope — omits signerEdPublicKey / signerMlDsaPublicKey.
58
+ *
59
+ * Safe to use ONLY when the verifier resolves the signer's public keys from
60
+ * an external, trusted source (e.g. your MUID/key registry) rather than
61
+ * trusting whatever is embedded in the payload. This is the correct model
62
+ * for anything that already requires a live network round-trip to verify
63
+ * (like MajikSLink), but is NOT a drop-in replacement for offline/portable
64
+ * file signatures where self-containment matters.
65
+ */
66
+ export interface MajikSignatureCompactJSON {
67
+ v: 1;
68
+ signerId: string;
69
+ contentHash: string;
70
+ contentType?: string;
71
+ timestamp: string;
72
+ edSignature: string;
73
+ mlDsaSignature: string;
74
+ allowlistHash?: string;
75
+ }
56
76
  export interface MajikTSARequest {
57
77
  digest: {
58
78
  algorithm: "SHA-256";
@@ -4,7 +4,7 @@
4
4
  *
5
5
  */
6
6
  import type { MajikKey } from "@majikah/majik-key";
7
- import type { BatchFileInput, BatchSignOptions, BatchVerifyInput, BatchVerifyOptions, EnvelopeInfo, ExpectedSigner, FileVerifyResult, MajikSignatureEnvelopeJSON, MajikSignatureJSON, MajikSignerPublicKeys, MajikTimestamp, MajikTSARequest, SealInfo, SealVerificationResult, SignatoriesFilter, SignatoriesResult, SignatoryInfo, SignOptions, VerificationResult } from "./core/types";
7
+ import type { BatchFileInput, BatchSignOptions, BatchVerifyInput, BatchVerifyOptions, EnvelopeInfo, ExpectedSigner, FileVerifyResult, MajikSignatureCompactJSON, MajikSignatureEnvelopeJSON, MajikSignatureJSON, MajikSignerPublicKeys, MajikTimestamp, MajikTSARequest, SealInfo, SealVerificationResult, SignatoriesFilter, SignatoriesResult, SignatoryInfo, SignOptions, VerificationResult } from "./core/types";
8
8
  import { MajikSignatureEmbed } from "./core/embed/majik-embed";
9
9
  import type { ImageVerificationResult, ImageSignOptions, ImageSignatureStub } from "./core/stamp";
10
10
  import { MajikChainAnchor, MajikChainAnchorMemo } from "./anchor/types";
@@ -500,5 +500,28 @@ export declare class MajikSignature {
500
500
  static getChainAnchors(file: Blob, options?: {
501
501
  mimeType?: string;
502
502
  }): Promise<MajikChainAnchor[]>;
503
+ /**
504
+ * Strip the embedded public keys, producing the wire/storage-optimized form.
505
+ * The verifier must supply the signer's public keys out-of-band via
506
+ * fromCompact() / verifyCompact() — never trust keys recovered from the
507
+ * compact payload itself, because there are none.
508
+ */
509
+ toCompact(): MajikSignatureCompactJSON;
510
+ /**
511
+ * Rehydrate a full MajikSignature from a compact payload + externally
512
+ * resolved public keys. Throws if signerId doesn't match the supplied keys —
513
+ * this is a cheap sanity check, not a substitute for verify().
514
+ */
515
+ static fromCompact(compact: MajikSignatureCompactJSON, publicKeys: Pick<MajikSignerPublicKeys, "edPublicKey" | "mlDsaPublicKey">): MajikSignature;
516
+ /**
517
+ * Verify content against a compact envelope. signerId is checked against
518
+ * publicKeys.signerId before any crypto runs, so a mismatched lookup fails
519
+ * fast with a clear reason instead of a cryptic signature failure.
520
+ *
521
+ * @example
522
+ * const keys = await resolvePublicKeysForMuid(slink.muid); // your registry
523
+ * const result = MajikSignature.verifyCompact(canonical, slink.signatureJSON, keys);
524
+ */
525
+ static verifyCompact(content: Uint8Array | string, compact: MajikSignatureCompactJSON, publicKeys: MajikSignerPublicKeys): VerificationResult;
503
526
  private static _isMajikKey;
504
527
  }
@@ -810,6 +810,71 @@ export class MajikSignature {
810
810
  static async getChainAnchors(file, options) {
811
811
  return MajikSignatureEmbed.getChainAnchors(file, options);
812
812
  }
813
+ // ── COMPACT FORMAT ─────────────────────────────────────────────────────────
814
+ /**
815
+ * Strip the embedded public keys, producing the wire/storage-optimized form.
816
+ * The verifier must supply the signer's public keys out-of-band via
817
+ * fromCompact() / verifyCompact() — never trust keys recovered from the
818
+ * compact payload itself, because there are none.
819
+ */
820
+ toCompact() {
821
+ return {
822
+ v: this._version,
823
+ signerId: this._signerId,
824
+ contentHash: this._contentHash,
825
+ contentType: this._contentType,
826
+ timestamp: this._timestamp,
827
+ edSignature: this._edSignature,
828
+ mlDsaSignature: this._mlDsaSignature,
829
+ allowlistHash: this._allowlistHash,
830
+ };
831
+ }
832
+ /**
833
+ * Rehydrate a full MajikSignature from a compact payload + externally
834
+ * resolved public keys. Throws if signerId doesn't match the supplied keys —
835
+ * this is a cheap sanity check, not a substitute for verify().
836
+ */
837
+ static fromCompact(compact, publicKeys) {
838
+ if (!publicKeys?.edPublicKey || !publicKeys?.mlDsaPublicKey) {
839
+ throw new MajikSignatureKeyError("fromCompact() requires the signer's public keys — resolve them by compact.signerId from your key registry / MUID service.");
840
+ }
841
+ const full = {
842
+ version: compact.v,
843
+ signerId: compact.signerId,
844
+ signerEdPublicKey: bytesToBase64(publicKeys.edPublicKey),
845
+ signerMlDsaPublicKey: bytesToBase64(publicKeys.mlDsaPublicKey),
846
+ contentHash: compact.contentHash,
847
+ contentType: compact.contentType,
848
+ timestamp: compact.timestamp,
849
+ edSignature: compact.edSignature,
850
+ mlDsaSignature: compact.mlDsaSignature,
851
+ allowlistHash: compact.allowlistHash,
852
+ };
853
+ return MajikSignature.fromJSON(full);
854
+ }
855
+ /**
856
+ * Verify content against a compact envelope. signerId is checked against
857
+ * publicKeys.signerId before any crypto runs, so a mismatched lookup fails
858
+ * fast with a clear reason instead of a cryptic signature failure.
859
+ *
860
+ * @example
861
+ * const keys = await resolvePublicKeysForMuid(slink.muid); // your registry
862
+ * const result = MajikSignature.verifyCompact(canonical, slink.signatureJSON, keys);
863
+ */
864
+ static verifyCompact(content, compact, publicKeys) {
865
+ if (compact.signerId !== publicKeys.signerId) {
866
+ return {
867
+ valid: false,
868
+ signerId: compact.signerId,
869
+ contentHash: compact.contentHash,
870
+ timestamp: compact.timestamp,
871
+ contentType: compact.contentType,
872
+ reason: `signerId mismatch: envelope is "${compact.signerId}", provided publicKeys are for "${publicKeys.signerId}"`,
873
+ };
874
+ }
875
+ const full = MajikSignature.fromCompact(compact, publicKeys);
876
+ return MajikSignature.verify(content, full, publicKeys);
877
+ }
813
878
  // ── Private helpers ───────────────────────────────────────────────────────
814
879
  static _isMajikKey(v) {
815
880
  return typeof v.fingerprint === "string";
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@majikah/majik-signature",
3
3
  "type": "module",
4
4
  "description": "Majik Signature is a hybrid post-quantum content signing and verification library for the Majikah ecosystem. Built on top of Majik Key, it provides tamper-proof, forgery-resistant digital signatures for any content format — using a dual-algorithm architecture that combines classical Ed25519 with post-quantum ML-DSA-87 (FIPS-204).",
5
- "version": "0.2.7",
5
+ "version": "0.2.8",
6
6
  "license": "Apache-2.0",
7
7
  "author": "Zelijah",
8
8
  "main": "./dist/index.js",