@majikah/majik-signature 0.0.1 → 0.0.2

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.
Files changed (35) hide show
  1. package/README.md +369 -372
  2. package/dist/core/embed/constants.d.ts +25 -0
  3. package/dist/core/embed/constants.js +27 -0
  4. package/dist/core/embed/fallback.d.ts +18 -0
  5. package/dist/core/embed/fallback.js +31 -0
  6. package/dist/core/embed/handlers/flac.d.ts +29 -0
  7. package/dist/core/embed/handlers/flac.js +186 -0
  8. package/dist/core/embed/handlers/jpeg.d.ts +23 -0
  9. package/dist/core/embed/handlers/jpeg.js +145 -0
  10. package/dist/core/embed/handlers/mkv.d.ts +22 -0
  11. package/dist/core/embed/handlers/mkv.js +45 -0
  12. package/dist/core/embed/handlers/mp3.d.ts +32 -0
  13. package/dist/core/embed/handlers/mp3.js +200 -0
  14. package/dist/core/embed/handlers/mp4.d.ts +27 -0
  15. package/dist/core/embed/handlers/mp4.js +173 -0
  16. package/dist/core/embed/handlers/office.d.ts +24 -0
  17. package/dist/core/embed/handlers/office.js +96 -0
  18. package/dist/core/embed/handlers/pdf.d.ts +21 -0
  19. package/dist/core/embed/handlers/pdf.js +183 -0
  20. package/dist/core/embed/handlers/png.d.ts +23 -0
  21. package/dist/core/embed/handlers/png.js +138 -0
  22. package/dist/core/embed/handlers/text.d.ts +29 -0
  23. package/dist/core/embed/handlers/text.js +102 -0
  24. package/dist/core/embed/handlers/wav.d.ts +24 -0
  25. package/dist/core/embed/handlers/wav.js +129 -0
  26. package/dist/core/embed/majik-embed.d.ts +85 -0
  27. package/dist/core/embed/majik-embed.js +174 -0
  28. package/dist/core/embed/registry.d.ts +25 -0
  29. package/dist/core/embed/registry.js +36 -0
  30. package/dist/core/embed/utils.d.ts +40 -0
  31. package/dist/core/embed/utils.js +251 -0
  32. package/dist/core/types.d.ts +79 -2
  33. package/dist/majik-signature.d.ts +84 -0
  34. package/dist/majik-signature.js +97 -0
  35. package/package.json +4 -2
package/README.md CHANGED
@@ -4,58 +4,30 @@
4
4
 
5
5
  **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 — plaintext, JSON, PDF, audio, video, binary — using a dual-algorithm architecture that combines classical Ed25519 with post-quantum ML-DSA-87 (FIPS-204).
6
6
 
7
+ **Majik Signature now includes built-in file embedding** — sign any file and embed the signature directly into its native metadata. No sidecar files needed. PDFs stay PDFs, WAVs stay WAVs, MP4s stay MP4s.
8
+
7
9
  ![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)
8
10
 
9
11
  ---
10
12
 
11
13
  - [Majik Signature](#majik-signature)
12
14
  - [Security Architecture](#security-architecture)
13
- - [1. Hybrid Dual-Algorithm Signing](#1-hybrid-dual-algorithm-signing)
14
- - [2. Canonical Payload Binding](#2-canonical-payload-binding)
15
- - [3. Content-Agnostic Hashing](#3-content-agnostic-hashing)
16
15
  - [Overview](#overview)
17
- - [What is a Majik Signature?](#what-is-a-majik-signature)
18
- - [Use Cases](#use-cases)
19
16
  - [Features](#features)
20
- - [Security \& Post-Quantum Readiness](#security--post-quantum-readiness)
21
- - [Content Format Support](#content-format-support)
22
- - [Developer Experience](#developer-experience)
23
- - [Serialization \& Portability](#serialization--portability)
24
17
  - [Installation](#installation)
25
18
  - [Quick Start](#quick-start)
19
+ - [File Embedding — Quick Start](#file-embedding--quick-start)
26
20
  - [API Reference](#api-reference)
27
- - [Static Methods](#static-methods)
28
- - [`MajikSignature.sign(content, key, options?)`](#majiksignaturesigncontent-key-options)
29
- - [`MajikSignature.verify(content, signature, publicKeys)`](#majiksignatureverifycontent-signature-publickeys)
30
- - [`MajikSignature.verifyWithKey(content, signature, key)`](#majiksignatureverifywithkeycontent-signature-key)
31
- - [`MajikSignature.publicKeysFromMajikKey(key)`](#majiksignaturepublickeysfrommajikkeykey)
32
- - [`MajikSignature.fromJSON(json)`](#majiksignaturefromjsonjson)
33
- - [`MajikSignature.deserialize(base64)`](#majiksignaturedeserializebase64)
21
+ - [Content Signing (bytes/strings)](#content-signing-bytesstrings)
22
+ - [File Embedding](#file-embedding)
23
+ - [Lower-Level Embed API](#lower-level-embed-api)
34
24
  - [Instance Methods](#instance-methods)
35
- - [`validate()`](#validate)
36
- - [`isValid()`](#isvalid)
37
- - [`extractPublicKeys()`](#extractpublickeys)
38
- - [`toJSON()`](#tojson)
39
- - [`serialize()`](#serialize)
40
- - [`toString()`](#tostring)
41
25
  - [Getters](#getters)
26
+ - [Supported File Formats](#supported-file-formats)
42
27
  - [Usage Examples](#usage-examples)
43
- - [Example 1: Sign and Verify a Text Document](#example-1-sign-and-verify-a-text-document)
44
- - [Example 2: Sign and Verify a Binary File](#example-2-sign-and-verify-a-binary-file)
45
- - [Example 3: Sign a JSON Payload](#example-3-sign-a-json-payload)
46
- - [Example 4: Serialize and Store a Signature](#example-4-serialize-and-store-a-signature)
47
- - [Example 5: Verify from Stored Signature](#example-5-verify-from-stored-signature)
48
- - [Example 6: Verify Using Only Public Keys](#example-6-verify-using-only-public-keys)
49
- - [Example 7: Sign Audio or Video Content](#example-7-sign-audio-or-video-content)
50
28
  - [Signature Envelope](#signature-envelope)
51
29
  - [Security Considerations](#security-considerations)
52
- - [What is Guaranteed](#what-is-guaranteed)
53
- - [What is Your Responsibility](#what-is-your-responsibility)
54
- - [What NOT to Do](#what-not-to-do)
55
- - [What TO Do](#what-to-do)
56
30
  - [Related Projects](#related-projects)
57
- - [Majik Key](#majik-key)
58
- - [Majik Message](#majik-message)
59
31
  - [Contributing](#contributing)
60
32
  - [License](#license)
61
33
  - [Author](#author)
@@ -92,18 +64,21 @@ Both signatures cover a **domain-separated canonical payload** that binds togeth
92
64
  | `ct` | Content type (advisory) |
93
65
  | `hash` | SHA-256 of the original content, base64 |
94
66
 
95
- This binding means a valid signature cannot be:
96
- - Reused on different content (hash binding)
97
- - Transferred to a different signer identity (id binding)
98
- - Replayed with a modified timestamp (ts binding)
99
- - Forged without both private keys
67
+ This binding means a valid signature cannot be reused on different content, transferred to a different signer, replayed with a modified timestamp, or forged without both private keys.
100
68
 
101
69
  ### 3. Content-Agnostic Hashing
102
70
 
103
- Content is never embedded in the envelope. Only its SHA-256 hash is signed. This means:
104
- - A 500 MB video signs at the same speed as a 10-byte string
105
- - Any format binary, text, JSON, PDF, audio, video — is supported identically
106
- - The original content travels separately; the signature is a portable proof
71
+ Content is never embedded in the envelope only its SHA-256 hash is signed. This means a 500 MB video signs at the same speed as a 10-byte string, and any format is supported identically.
72
+
73
+ ### 4. File Embedding Integrity
74
+
75
+ When a signature is embedded into a file, it always covers the **original file bytes before embedding**. Verification automatically strips the embedded signature before re-hashing, so the round-trip is always:
76
+
77
+ ```
78
+ sign(originalBytes) → embed into file → extract → strip → verify(originalBytes)
79
+ ```
80
+
81
+ Re-signing the same file is always safe and idempotent — the existing signature is stripped before the new one is created.
107
82
 
108
83
  ---
109
84
 
@@ -111,11 +86,7 @@ Content is never embedded in the envelope. Only its SHA-256 hash is signed. This
111
86
 
112
87
  ### What is a Majik Signature?
113
88
 
114
- A Majik Signature is a cryptographic proof that:
115
- - A specific piece of content (file, document, message, media) was produced or approved by the holder of a specific **Majik Key** account
116
- - The content has not been modified since it was signed
117
- - The signature cannot be forged without access to the signer's private keys
118
- - The signature remains valid against future quantum computing threats
89
+ A Majik Signature is a cryptographic proof that a specific piece of content was produced or approved by the holder of a specific **Majik Key** account, that the content has not been modified since it was signed, and that the signature remains valid against future quantum computing threats.
119
90
 
120
91
  Verification is fully **public** — anyone with the signer's public keys can verify. No private key is ever needed for verification.
121
92
 
@@ -125,7 +96,7 @@ Verification is fully **public** — anyone with the signer's public keys can ve
125
96
  - **File Integrity**: Detect any tampering or modification to distributed files
126
97
  - **API Payload Signing**: Sign JSON responses or requests for non-repudiation
127
98
  - **Document Authentication**: Certify legal documents, contracts, or records
128
- - **Media Certification**: Stamp audio, video, or image files as authentic originals
99
+ - **Media Certification**: Stamp audio, video, or image files as authentic originals — with the signature embedded directly in the file's metadata
129
100
  - **Software Distribution**: Sign release artifacts to prove they come from the original author
130
101
  - **Majikah Ecosystem**: Integrate with Majik Message and other Majikah products for identity-bound content
131
102
 
@@ -144,34 +115,36 @@ Verification is fully **public** — anyone with the signer's public keys can ve
144
115
 
145
116
  ### Content Format Support
146
117
 
147
- - **Plain text** — UTF-8 strings
148
- - **JSON** — Any JSON-serializable payload
149
- - **Binary** — `Uint8Array` of any format
150
- - **PDF** — Read as raw bytes
151
- - **Audio** — WAV, MP3, FLAC, and any other audio format
152
- - **Video** — MP4, MOV, and any other video format
153
- - **Images** — PNG, JPEG, WEBP, and others
154
- - **Any file** — If you can read it into a `Uint8Array`, it can be signed
118
+ - **Plain text**, **JSON**, **Binary** `Uint8Array` or `string`
119
+ - **PDF, PNG, JPEG** — Signature embedded in native metadata (visible in File → Properties for PDF)
120
+ - **WAV, MP3, FLAC** — Embedded in RIFF/ID3/Vorbis metadata
121
+ - **MP4, MOV, M4A, M4V** — Embedded in `moov/udta` box
122
+ - **DOCX, XLSX, PPTX, ODF** — Embedded as a file entry inside the ZIP container
123
+ - **MKV, WebM** — Embedded via append-safe trailer
124
+ - **HTML, Markdown, JSON, plain text, source code** — Appended comment block
125
+ - **Any other format** — Universal binary trailer (self-describing, cleanly strippable)
155
126
 
156
127
  ### Developer Experience
157
128
 
158
129
  - **First-Class TypeScript Support**: Full type definitions for all interfaces and classes
159
- - **Simple Two-Method Core API**: `sign()` and `verify()` cover the primary use case
160
- - **Convenience Helpers**: `verifyWithKey()` and `publicKeysFromMajikKey()` for common patterns
130
+ - **Simple Core API**: `sign()` and `verify()` for bytes/strings; `signFile()` and `verifyFile()` for files
131
+ - **One-liner file signing**: `MajikSignature.signFile(blob, key)` — sign and embed in a single call
132
+ - **Format auto-detection**: MIME type and magic-byte sniffing — no manual format hints required
133
+ - **Idempotent re-signing**: Safely re-sign any file without accumulating stacked signatures
161
134
  - **Structured Errors**: Typed error hierarchy for precise error handling
162
- - **Self-Validation**: `isValid()` and `validate()` for envelope integrity checks
163
- - **Isomorphic**: Works in Node.js and modern browser environments
135
+ - **Isomorphic**: Works in Node.js and modern browser environments (no native deps)
164
136
 
165
137
  ### Serialization & Portability
166
138
 
167
139
  - **JSON Envelope**: Full `toJSON()` / `fromJSON()` round-trip
168
140
  - **Base64 Serialization**: `serialize()` / `deserialize()` for compact transport
169
- - **Embeddable**: Base64 signature fits in database fields, HTTP headers, file metadata, or sidecar files
141
+ - **File-embedded**: Signature lives inside the file itself no sidecar files needed
170
142
  - **Self-Contained**: Envelope includes signer's public keys — verifiable without a key registry
171
143
 
172
144
  ---
173
145
 
174
146
  ## Installation
147
+
175
148
  ```bash
176
149
  # Using npm
177
150
  npm install @majikah/majik-signature
@@ -180,17 +153,19 @@ npm install @majikah/majik-signature
180
153
  npm install @majikah/majik-key
181
154
  ```
182
155
 
156
+ No native bindings. Works in Node.js 18+, all modern browsers, Deno, and Bun.
157
+
183
158
  ---
184
159
 
185
160
  ## Quick Start
161
+
186
162
  ```typescript
187
163
  import { MajikKey } from '@majikah/majik-key';
188
164
  import { MajikSignature, CONTENT_TYPES } from '@majikah/majik-signature';
189
165
 
190
- // ── Step 1: Create and unlock a MajikKey ──────────────────────────────────────
166
+ // ── Step 1: Create and unlock a MajikKey ─────────────────────────────────────
191
167
  const mnemonic = MajikKey.generateMnemonic();
192
168
  const key = await MajikKey.create(mnemonic, 'my-passphrase', 'My Signing Key');
193
- // key is unlocked after create() — signing keys are ready
194
169
 
195
170
  // ── Step 2: Sign content ──────────────────────────────────────────────────────
196
171
  const document = 'This is the original content of my document.';
@@ -206,84 +181,97 @@ console.log('Timestamp:', signature.timestamp);
206
181
 
207
182
  // ── Step 3: Serialize for storage or transport ────────────────────────────────
208
183
  const serialized = signature.serialize(); // base64 string
209
- // Store in a database, embed in a file, send via HTTP header, etc.
210
184
 
211
185
  // ── Step 4: Verify (no private key needed) ────────────────────────────────────
212
186
  const publicKeys = MajikSignature.publicKeysFromMajikKey(key);
213
187
  const result = MajikSignature.verify(document, signature, publicKeys);
214
188
 
215
- console.log('Valid:', result.valid); // true
189
+ console.log('Valid:', result.valid); // true
216
190
  console.log('Signer:', result.signerId);
217
- console.log('Hash:', result.contentHash);
218
191
 
219
- // ── Shorthand: verify directly against a MajikKey ────────────────────────────
192
+ // Shorthand verify directly against a MajikKey
220
193
  const result2 = MajikSignature.verifyWithKey(document, signature, key);
221
- console.log('Valid:', result2.valid); // true
194
+ console.log('Valid:', result2.valid); // true
195
+ ```
196
+
197
+ ---
198
+
199
+ ## File Embedding — Quick Start
200
+
201
+ ```typescript
202
+ import { MajikKey } from '@majikah/majik-key';
203
+ import { MajikSignature } from '@majikah/majik-signature';
204
+
205
+ // ── Sign a file and embed the signature into it ───────────────────────────────
206
+ const { blob: signedBlob } = await MajikSignature.signFile(file, key);
207
+ // signedBlob is the same format as file — PDF stays PDF, WAV stays WAV, etc.
208
+ // The signature is embedded in the file's native metadata.
209
+
210
+ // ── Verify the embedded signature later ──────────────────────────────────────
211
+ const result = await MajikSignature.verifyFile(signedBlob, key);
212
+ if (result.valid) {
213
+ console.log('Verified — signed by:', result.signerId);
214
+ console.log('At:', result.timestamp);
215
+ console.log('Handler used:', result.handler); // e.g. "PDF", "WAV", "MP4/MOV"
216
+ }
217
+
218
+ // ── Check if a file is signed (without verifying) ────────────────────────────
219
+ const signed = await MajikSignature.isSigned(file);
220
+
221
+ // ── Extract the embedded signature as a typed instance ───────────────────────
222
+ const sig = await MajikSignature.extractFrom(signedBlob);
223
+ if (sig) {
224
+ console.log(sig.signerId, sig.timestamp, sig.contentHash);
225
+ }
226
+
227
+ // ── Get the original clean file (signature removed) ──────────────────────────
228
+ const originalBlob = await MajikSignature.stripFrom(signedBlob);
229
+
230
+ // ── Embed an already-computed signature into a file ──────────────────────────
231
+ const sig2 = await MajikSignature.sign(await file.arrayBuffer(), key);
232
+ const signedBlob2 = await sig2.embedIn(file);
222
233
  ```
223
234
 
224
235
  ---
225
236
 
226
237
  ## API Reference
227
238
 
228
- ### Static Methods
239
+ ### Content Signing (bytes/strings)
229
240
 
230
241
  #### `MajikSignature.sign(content, key, options?)`
231
242
 
232
- Sign content with an unlocked MajikKey. Produces a hybrid Ed25519 + ML-DSA-87 signature.
233
-
234
- The key must be unlocked and must have signing keys (`key.hasSigningKeys === true`). Keys created with the current version of Majik Key always include signing keys. Legacy keys can be upgraded by re-importing via `importFromMnemonicBackup()`.
243
+ Sign raw bytes or a string with an unlocked MajikKey.
235
244
 
236
245
  **Parameters:**
237
246
  - `content: Uint8Array | string` — Content to sign. Strings are UTF-8 encoded before hashing.
238
247
  - `key: MajikKey` — An unlocked MajikKey with signing keys present.
239
- - `options?: SignOptions` — Optional configuration.
248
+ - `options?: SignOptions`
240
249
  - `contentType?: string` — Advisory label (e.g. `"audio/wav"`, `"application/pdf"`). See `CONTENT_TYPES`.
241
- - `timestamp?: string` — ISO 8601 timestamp override. Defaults to `new Date().toISOString()`. Useful for deterministic tests.
250
+ - `timestamp?: string` — ISO 8601 timestamp override. Defaults to `new Date().toISOString()`.
242
251
 
243
- **Returns:** `Promise<MajikSignature>` — A new MajikSignature instance ready to serialize or verify.
252
+ **Returns:** `Promise<MajikSignature>`
244
253
 
245
- **Throws:** `MajikSignatureKeyError` if the key is locked or has no signing keys. `MajikSignatureError` on any other failure.
246
-
247
- **Example:**
248
- ```typescript
249
- const signature = await MajikSignature.sign(content, key, {
250
- contentType: 'application/pdf',
251
- });
252
- ```
254
+ **Throws:** `MajikSignatureKeyError` if the key is locked or has no signing keys.
253
255
 
254
256
  ---
255
257
 
256
258
  #### `MajikSignature.verify(content, signature, publicKeys)`
257
259
 
258
- Verify a signature against content and the signer's public keys.
259
-
260
- No private key is needed. Both Ed25519 and ML-DSA-87 must pass. Returns a structured result rather than throwing on invalid signatures — only throws on unexpected internal errors.
260
+ Verify a signature against content and the signer's public keys. Both Ed25519 and ML-DSA-87 must pass.
261
261
 
262
262
  **Parameters:**
263
- - `content: Uint8Array | string` — The original content that was signed. Must be byte-for-byte identical to what was passed to `sign()`.
263
+ - `content: Uint8Array | string` — The original content that was signed.
264
264
  - `signature: MajikSignature | MajikSignatureJSON` — The signature to verify.
265
- - `publicKeys: MajikSignerPublicKeys` — Signer's Ed25519 (32 bytes) and ML-DSA-87 (2592 bytes) public keys.
265
+ - `publicKeys: MajikSignerPublicKeys` — Signer's Ed25519 and ML-DSA-87 public keys.
266
266
 
267
267
  **Returns:** `VerificationResult`
268
268
  ```typescript
269
269
  {
270
- valid: boolean; // true only if both Ed25519 and ML-DSA-87 pass
271
- signerId: string; // signer fingerprint from the envelope
272
- contentHash: string; // SHA-256 of content, base64
273
- timestamp: string; // ISO 8601 from the envelope
274
- contentType?: string; // advisory content type if present
275
- }
276
- ```
277
-
278
- **Throws:** `MajikSignatureVerificationError` on unexpected internal failure.
279
-
280
- **Example:**
281
- ```typescript
282
- const result = MajikSignature.verify(content, signature, publicKeys);
283
- if (result.valid) {
284
- console.log('Verified — signed by:', result.signerId);
285
- } else {
286
- console.log('Invalid signature');
270
+ valid: boolean;
271
+ signerId: string;
272
+ contentHash: string;
273
+ timestamp: string;
274
+ contentType?: string;
287
275
  }
288
276
  ```
289
277
 
@@ -291,170 +279,193 @@ if (result.valid) {
291
279
 
292
280
  #### `MajikSignature.verifyWithKey(content, signature, key)`
293
281
 
294
- Convenience method — verify content directly against a MajikKey instance. Extracts public keys automatically. Works on both locked and unlocked keys.
295
-
296
- **Parameters:**
297
- - `content: Uint8Array | string` — The original content.
298
- - `signature: MajikSignature | MajikSignatureJSON` — The signature to verify.
299
- - `key: MajikKey` — The MajikKey to verify against. Does not need to be unlocked.
300
-
301
- **Returns:** `VerificationResult` — same as `verify()`.
302
-
303
- **Example:**
304
- ```typescript
305
- const result = MajikSignature.verifyWithKey(content, signature, key);
306
- console.log('Valid:', result.valid);
307
- ```
282
+ Convenience — verify directly against a MajikKey instance. Works on locked keys.
308
283
 
309
284
  ---
310
285
 
311
286
  #### `MajikSignature.publicKeysFromMajikKey(key)`
312
287
 
313
- Extract the public keys needed for `verify()` from a MajikKey. Works on locked keys — only reads public fields.
314
-
315
- **Parameters:**
316
- - `key: MajikKey` — Any MajikKey with signing keys (locked or unlocked).
288
+ Extract public keys from a MajikKey for use with `verify()`. Works on locked keys.
317
289
 
318
290
  **Returns:** `MajikSignerPublicKeys`
319
291
  ```typescript
320
292
  {
321
- signerId: string; // MajikKey fingerprint
322
- edPublicKey: Uint8Array; // Ed25519 public key (32 bytes)
323
- mlDsaPublicKey: Uint8Array; // ML-DSA-87 public key (2592 bytes)
293
+ signerId: string;
294
+ edPublicKey: Uint8Array; // 32 bytes
295
+ mlDsaPublicKey: Uint8Array; // 2592 bytes
324
296
  }
325
297
  ```
326
298
 
327
- **Throws:** `MajikSignatureKeyError` if the key has no signing public keys.
299
+ ---
328
300
 
329
- **Example:**
330
- ```typescript
331
- const publicKeys = MajikSignature.publicKeysFromMajikKey(key);
332
- // Store publicKeys or pass to verify()
333
- ```
301
+ #### `MajikSignature.fromJSON(json)` / `MajikSignature.deserialize(base64)`
302
+
303
+ Reconstruct a `MajikSignature` from stored JSON or base64.
334
304
 
335
305
  ---
336
306
 
337
- #### `MajikSignature.fromJSON(json)`
307
+ ### File Embedding
308
+
309
+ These methods sign or verify files with the signature embedded directly in the file. The file format is auto-detected from magic bytes — no manual hints needed in most cases.
338
310
 
339
- Reconstruct a MajikSignature from its JSON representation. Validates envelope structure on parse.
311
+ ---
312
+
313
+ #### `MajikSignature.signFile(file, key, options?)`
314
+
315
+ Sign a file and embed the signature into it in one call. Strips any existing signature before signing so re-signing is always safe.
340
316
 
341
317
  **Parameters:**
342
- - `json: MajikSignatureJSON | string` — JSON object or JSON string.
318
+ - `file: Blob` — The file to sign.
319
+ - `key: MajikKey` — An unlocked MajikKey with signing keys.
320
+ - `options?`
321
+ - `contentType?: string` — Advisory label stored in the envelope.
322
+ - `timestamp?: string` — ISO 8601 override.
323
+ - `mimeType?: string` — Override auto-detected MIME type.
343
324
 
344
- **Returns:** `MajikSignature`
325
+ **Returns:** `Promise<{ blob: Blob; signature: MajikSignature; handler: string; mimeType: string }>`
345
326
 
346
- **Throws:** `MajikSignatureSerializationError` on invalid or malformed JSON.
327
+ - `blob` The signed file. Same format as the input.
328
+ - `signature` — The `MajikSignature` instance, if you need it separately.
329
+ - `handler` — Which format handler was used (e.g. `"PDF"`, `"WAV"`, `"MP4/MOV"`).
330
+ - `mimeType` — The detected MIME type.
347
331
 
348
332
  **Example:**
349
333
  ```typescript
350
- const signature = MajikSignature.fromJSON(storedJson);
334
+ const { blob: signedPdf } = await MajikSignature.signFile(pdfBlob, key);
335
+ // signedPdf is a valid PDF with the signature in its /Info dict + XMP metadata
351
336
  ```
352
337
 
353
338
  ---
354
339
 
355
- #### `MajikSignature.deserialize(base64)`
340
+ #### `MajikSignature.verifyFile(file, keyOrPublicKeys, options?)`
356
341
 
357
- Reconstruct a MajikSignature from a base64 serialized string produced by `serialize()`.
342
+ Verify a file's embedded signature. Accepts either a `MajikKey` instance or raw `MajikSignerPublicKeys`.
358
343
 
359
344
  **Parameters:**
360
- - `base64: string` — Base64 string from a previous `serialize()` call.
361
-
362
- **Returns:** `MajikSignature`
345
+ - `file: Blob` — The signed file.
346
+ - `keyOrPublicKeys: MajikKey | MajikSignerPublicKeys` — The key or public keys to verify against.
347
+ - `options?`
348
+ - `expectedSignerId?: string` — If provided, checks `signerId` before running crypto.
349
+ - `mimeType?: string` — Override auto-detected MIME type.
363
350
 
364
- **Throws:** `MajikSignatureSerializationError` on invalid input.
351
+ **Returns:** `Promise<VerificationResult & { handler?: string }>`
365
352
 
366
353
  **Example:**
367
354
  ```typescript
368
- const signature = MajikSignature.deserialize(storedBase64);
355
+ const result = await MajikSignature.verifyFile(signedWav, key);
356
+ if (result.valid) {
357
+ console.log('Signed by:', result.signerId);
358
+ console.log('At:', result.timestamp);
359
+ }
369
360
  ```
370
361
 
371
362
  ---
372
363
 
373
- ### Instance Methods
374
-
375
- #### `validate()`
364
+ #### `MajikSignature.extractFrom(file, options?)`
376
365
 
377
- Validate the envelope's internal structure without performing cryptographic verification. Checks all required fields, base64 lengths, and timestamp format. Useful before storing or transmitting.
366
+ Extract the embedded signature as a fully typed `MajikSignature` instance. Returns `null` if no signature is found.
378
367
 
379
- **Returns:** `void`
380
-
381
- **Throws:** `MajikSignatureValidationError` on any structural problem.
368
+ **Returns:** `Promise<MajikSignature | null>`
382
369
 
383
370
  **Example:**
384
371
  ```typescript
385
- signature.validate(); // throws if structurally invalid
372
+ const sig = await MajikSignature.extractFrom(file);
373
+ if (sig) {
374
+ console.log(sig.signerId, sig.timestamp, sig.contentHash);
375
+ }
386
376
  ```
387
377
 
388
378
  ---
389
379
 
390
- #### `isValid()`
380
+ #### `MajikSignature.stripFrom(file, options?)`
391
381
 
392
- Returns `true` if the envelope is structurally valid, `false` otherwise. Never throws safe to use as a boolean guard anywhere.
382
+ Return a clean copy of the file with any embedded signature removed. The returned bytes are exactly what was originally signed.
393
383
 
394
- **Returns:** `boolean`
384
+ **Returns:** `Promise<Blob>`
395
385
 
396
386
  **Example:**
397
387
  ```typescript
398
- if (!signature.isValid()) {
399
- console.error('Envelope is malformed');
400
- }
388
+ const original = await MajikSignature.stripFrom(signedMp4);
389
+ // original bytes are what was hashed when the signature was created
401
390
  ```
402
391
 
403
392
  ---
404
393
 
405
- #### `extractPublicKeys()`
406
-
407
- Extract the signer's public keys from the envelope itself.
394
+ #### `MajikSignature.isSigned(file, options?)`
408
395
 
409
- > ⚠️ **Important:** Public keys embedded in the envelope are self-reported by the signer. Always cross-check `signerId` against a trusted source (e.g. a known `MajikKey.fingerprint`) before trusting extracted keys for verification. Use `verifyWithKey()` or keep your own key store when possible.
410
-
411
- **Returns:** `MajikSignerPublicKeys`
396
+ Check whether a file contains an embedded signature. Does not verify purely a structural presence check. Useful as a fast guard before verification.
412
397
 
413
- **Throws:** `MajikSignatureKeyError` if keys cannot be decoded.
398
+ **Returns:** `Promise<boolean>`
414
399
 
415
400
  **Example:**
416
401
  ```typescript
417
- const keys = signature.extractPublicKeys();
418
- // Cross-check keys.signerId against a known trusted fingerprint
402
+ if (await MajikSignature.isSigned(file)) {
403
+ const result = await MajikSignature.verifyFile(file, key);
404
+ }
419
405
  ```
420
406
 
421
407
  ---
422
408
 
423
- #### `toJSON()`
409
+ #### `signature.embedIn(file, options?)` *(instance method)*
424
410
 
425
- Export the full signature envelope as a plain JSON object.
411
+ Embed this `MajikSignature` instance into a file. Call on an existing instance when you have already signed the content separately.
426
412
 
427
- **Returns:** `MajikSignatureJSON`
413
+ > **Note:** The signature must have been created from the original file bytes **before** embedding. Use `signFile()` if you want signing and embedding together.
414
+
415
+ **Returns:** `Promise<Blob>`
428
416
 
429
417
  **Example:**
430
418
  ```typescript
431
- const json = signature.toJSON();
432
- await db.signatures.insert({ id: docId, sig: json });
419
+ const originalBytes = new Uint8Array(await file.arrayBuffer());
420
+ const sig = await MajikSignature.sign(originalBytes, key);
421
+ const signedBlob = await sig.embedIn(file);
433
422
  ```
434
423
 
435
424
  ---
436
425
 
437
- #### `serialize()`
426
+ ### Lower-Level Embed API
438
427
 
439
- Serialize the envelope to a compact base64 string. Suitable for embedding in database fields, HTTP headers, file metadata, or sidecar `.sig` files.
428
+ For advanced use cases custom handler registration, explicit format control, or accessing handler metadata the underlying `MajikSignatureEmbed` class is also exported.
440
429
 
441
- **Returns:** `string` — Base64 encoded envelope.
430
+ ```typescript
431
+ import { MajikSignatureEmbed } from '@majikah/majik-signature';
442
432
 
443
- **Throws:** `MajikSignatureSerializationError` on failure.
433
+ // Register a custom handler for an unsupported format
434
+ MajikSignatureEmbed.registry.register(new MyCustomHandler());
444
435
 
445
- **Example:**
446
- ```typescript
447
- const b64 = signature.serialize();
448
- res.setHeader('X-Majik-Signature', b64);
436
+ // List all registered handlers
437
+ console.log(MajikSignatureEmbed.listHandlers());
438
+ // ['PDF', 'PNG', 'JPEG', 'WAV', 'MP3', 'MP4/MOV', 'FLAC', 'MKV/WebM',
439
+ // 'Office (DOCX/XLSX/PPTX/ODF)', 'Text/Markup/Source',
440
+ // 'Fallback (Universal Trailer)']
441
+
442
+ // Force the Tier-2 trailer even for natively supported formats
443
+ const { blob } = await MajikSignatureEmbed.embed(file, sig, { forceFallback: true });
449
444
  ```
450
445
 
451
446
  ---
452
447
 
453
- #### `toString()`
448
+ ### Instance Methods
449
+
450
+ #### `validate()`
451
+ Validate the envelope's internal structure without performing cryptographic verification. Throws `MajikSignatureValidationError` on any structural problem.
454
452
 
455
- Alias for `serialize()`. Returns the base64 serialized envelope.
453
+ #### `isValid()`
454
+ Returns `true` if the envelope is structurally valid. Never throws — safe to use as a boolean guard.
455
+
456
+ #### `extractPublicKeys()`
457
+ Extract the signer's public keys from the envelope.
456
458
 
457
- **Returns:** `string`
459
+ > ⚠️ Public keys embedded in the envelope are self-reported by the signer. Always cross-check `signerId` against a trusted source before trusting extracted keys for verification.
460
+
461
+ #### `toJSON()`
462
+ Export the full signature envelope as a plain JSON object.
463
+
464
+ #### `serialize()`
465
+ Serialize the envelope to a compact base64 string. Suitable for embedding in database fields, HTTP headers, file metadata, or sidecar files.
466
+
467
+ #### `toString()`
468
+ Alias for `serialize()`.
458
469
 
459
470
  ---
460
471
 
@@ -474,250 +485,235 @@ Alias for `serialize()`. Returns the base64 serialized envelope.
474
485
 
475
486
  ---
476
487
 
477
- ## Usage Examples
488
+ ## Supported File Formats
478
489
 
479
- ### Example 1: Sign and Verify a Text Document
480
- ```typescript
481
- import { MajikKey } from '@majikah/majik-key';
482
- import { MajikSignature, CONTENT_TYPES } from '@majikah/majik-signature';
490
+ ### Tier 1 Native metadata
483
491
 
484
- async function signDocument() {
485
- const mnemonic = MajikKey.generateMnemonic();
486
- const key = await MajikKey.create(mnemonic, 'passphrase', 'Author Key');
492
+ The signature is stored in each format's built-in metadata container. The file remains structurally valid and the signature survives round-trips through standard tools.
487
493
 
488
- const document = `
489
- AGREEMENT
494
+ | Format | Embedding mechanism |
495
+ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------ |
496
+ | **PDF** | `/Info` dictionary custom key + XMP metadata stream. Visible in File → Properties in most PDF viewers. |
497
+ | **PNG** | `iTXt` chunk with keyword `majik-signature` |
498
+ | **JPEG / JPG** | Custom `APP15` marker segment |
499
+ | **WAV / WAVE** | RIFF `LIST INFO` chunk — `ISIG` entry |
500
+ | **MP3** | ID3v2 `TXXX` frame with description `MAJIK-SIGNATURE` |
501
+ | **MP4 / MOV / M4A / M4V** | `moov → udta → majk` box |
502
+ | **FLAC** | `VORBIS_COMMENT` block — `MAJIK-SIGNATURE=` field |
503
+ | **MKV / WebM** | Append-safe binary trailer |
504
+ | **DOCX / XLSX / PPTX** | `majik-signature.json` entry inside the ZIP container |
505
+ | **ODF (ODT/ODS/ODP)** | Same as OOXML — ZIP entry |
506
+ | **HTML / XML / SVG / Markdown** | `<!-- MAJIK-SIGNATURE-BEGIN -->` block appended at end |
507
+ | **Plain text / JSON / CSV / source code** | Same comment block |
490
508
 
491
- This agreement is entered into on January 1, 2026.
492
- Party A agrees to deliver the software by March 31, 2026.
493
- `;
509
+ ### Tier 2 Universal trailer
494
510
 
495
- // Sign
496
- const signature = await MajikSignature.sign(document, key, {
497
- contentType: CONTENT_TYPES.TEXT,
498
- });
511
+ For any format not covered above, a self-describing binary trailer is appended:
499
512
 
500
- console.log('✅ Document signed');
501
- console.log('Signer:', signature.signerId);
502
- console.log('At:', signature.timestamp);
503
-
504
- // Verify
505
- const result = MajikSignature.verifyWithKey(document, signature, key);
506
- console.log('✅ Verified:', result.valid); // true
513
+ ```
514
+ [original file bytes][signature JSON UTF-8][8-byte payload length LE][8-byte magic: MAJIKSIG]
515
+ ```
507
516
 
508
- // Tamper detection
509
- const tampered = document + ' (modified)';
510
- const tamperResult = MajikSignature.verifyWithKey(tampered, signature, key);
511
- console.log('❌ Tampered rejected:', tamperResult.valid); // false
512
- }
517
+ The magic bytes at the end allow detection and clean stripping from any file without knowing its format. Most parsers and players ignore trailing bytes.
513
518
 
514
- signDocument();
515
- ```
519
+ > **Re-mux warning:** For MKV/WebM and the Tier-2 fallback, the embedded signature will be stripped if the file is re-encoded or re-muxed through a tool that rewrites the container. For MP4, DOCX, and all Tier-1 native-metadata formats, the signature survives standard open → save round-trips.
516
520
 
517
521
  ---
518
522
 
519
- ### Example 2: Sign and Verify a Binary File
523
+ ## Usage Examples
524
+
525
+ ### Example 1: Sign and Verify a Text Document
520
526
  ```typescript
521
527
  import { MajikKey } from '@majikah/majik-key';
522
528
  import { MajikSignature, CONTENT_TYPES } from '@majikah/majik-signature';
523
- import { readFileSync } from 'fs';
524
-
525
- async function signFile() {
526
- const mnemonic = MajikKey.generateMnemonic();
527
- const key = await MajikKey.create(mnemonic, 'passphrase', 'Publisher Key');
528
529
 
529
- // Read any binary file — PDF, WAV, MP4, etc.
530
- const fileBytes = new Uint8Array(readFileSync('./release.zip'));
530
+ const mnemonic = MajikKey.generateMnemonic();
531
+ const key = await MajikKey.create(mnemonic, 'passphrase', 'Author Key');
531
532
 
532
- const signature = await MajikSignature.sign(fileBytes, key, {
533
- contentType: 'application/zip',
534
- });
533
+ const document = `
534
+ AGREEMENT
535
+ This agreement is entered into on January 1, 2026.
536
+ Party A agrees to deliver the software by March 31, 2026.
537
+ `;
535
538
 
536
- console.log('✅ File signed');
537
- console.log('Content Hash:', signature.contentHash);
539
+ const signature = await MajikSignature.sign(document, key, {
540
+ contentType: CONTENT_TYPES.TEXT,
541
+ });
538
542
 
539
- // Verification
540
- const result = MajikSignature.verifyWithKey(fileBytes, signature, key);
541
- console.log('✅ File verified:', result.valid); // true
542
- }
543
+ const result = MajikSignature.verifyWithKey(document, signature, key);
544
+ console.log('Valid:', result.valid); // true
543
545
 
544
- signFile();
546
+ // Tamper detection
547
+ const tampered = document + ' (modified)';
548
+ const tamperResult = MajikSignature.verifyWithKey(tampered, signature, key);
549
+ console.log('Tampered rejected:', tamperResult.valid); // false
545
550
  ```
546
551
 
547
552
  ---
548
553
 
549
- ### Example 3: Sign a JSON Payload
554
+ ### Example 2: Sign a File and Embed the Signature
550
555
  ```typescript
551
556
  import { MajikKey } from '@majikah/majik-key';
552
- import { MajikSignature, CONTENT_TYPES } from '@majikah/majik-signature';
557
+ import { MajikSignature } from '@majikah/majik-signature';
553
558
 
554
- async function signJson() {
555
- const mnemonic = MajikKey.generateMnemonic();
556
- const key = await MajikKey.create(mnemonic, 'passphrase', 'API Key');
557
-
558
- const payload = {
559
- userId: 'usr_abc123',
560
- action: 'transfer',
561
- amount: 1000,
562
- currency: 'USD',
563
- nonce: crypto.randomUUID(),
564
- };
565
-
566
- // Always sign the canonical string — agree on stringify format
567
- const content = JSON.stringify(payload);
568
-
569
- const signature = await MajikSignature.sign(content, key, {
570
- contentType: CONTENT_TYPES.JSON,
571
- });
572
-
573
- // Attach to the API response
574
- const response = {
575
- data: payload,
576
- signature: signature.toJSON(),
577
- };
578
-
579
- // On the receiving end — verify before processing
580
- const result = MajikSignature.verifyWithKey(
581
- JSON.stringify(response.data),
582
- response.signature,
583
- key,
584
- );
585
-
586
- console.log('✅ Payload verified:', result.valid); // true
587
- }
559
+ const mnemonic = MajikKey.generateMnemonic();
560
+ const key = await MajikKey.create(mnemonic, 'passphrase', 'Artist Key');
588
561
 
589
- signJson();
562
+ // Works for any file — PDF, WAV, MP3, MP4, PNG, DOCX, etc.
563
+ const { blob: signedFile, handler } = await MajikSignature.signFile(file, key);
564
+
565
+ console.log('Signed using handler:', handler);
566
+ // e.g. "PDF", "WAV", "MP4/MOV", "Office (DOCX/XLSX/PPTX/ODF)"
567
+
568
+ // The signed file is the same format — upload or save it directly
569
+ await uploadFile(signedFile);
590
570
  ```
591
571
 
592
572
  ---
593
573
 
594
- ### Example 4: Serialize and Store a Signature
574
+ ### Example 3: Verify an Embedded Signature
595
575
  ```typescript
596
576
  import { MajikKey } from '@majikah/majik-key';
597
577
  import { MajikSignature } from '@majikah/majik-signature';
598
578
 
599
- async function storeSignature() {
600
- const mnemonic = MajikKey.generateMnemonic();
601
- const key = await MajikKey.create(mnemonic, 'passphrase', 'Storage Key');
579
+ // key does NOT need to be unlocked for verification
580
+ const key = MajikKey.fromJSON(storedKeyJson);
602
581
 
603
- const content = 'Original content of the certified document.';
604
- const signature = await MajikSignature.sign(content, key);
582
+ const result = await MajikSignature.verifyFile(downloadedFile, key);
605
583
 
606
- // Option A: Store as JSON (in a database column, sidecar file, etc.)
607
- const json = signature.toJSON();
608
- localStorage.setItem('doc_sig_001', JSON.stringify(json));
584
+ if (result.valid) {
585
+ console.log('Authentic. Signed by:', result.signerId);
586
+ console.log('Signed at:', result.timestamp);
587
+ } else {
588
+ console.log('Invalid or tampered:', result.reason);
589
+ }
590
+ ```
609
591
 
610
- // Option B: Store as base64 (in an HTTP header, metadata field, etc.)
611
- const b64 = signature.serialize();
612
- localStorage.setItem('doc_sig_001_b64', b64);
592
+ ---
613
593
 
614
- console.log('✅ Signature stored in both formats');
615
- console.log('JSON size (approx):', JSON.stringify(json).length, 'bytes');
616
- console.log('Base64 size (approx):', b64.length, 'bytes');
617
- }
594
+ ### Example 4: Sign a Binary File (Node.js)
595
+ ```typescript
596
+ import { MajikKey } from '@majikah/majik-key';
597
+ import { MajikSignature, CONTENT_TYPES } from '@majikah/majik-signature';
598
+ import { readFileSync } from 'fs';
599
+
600
+ const key = await MajikKey.create(mnemonic, 'passphrase', 'Publisher Key');
601
+ const fileBytes = new Uint8Array(readFileSync('./release.zip'));
602
+
603
+ // Option A: Sign bytes, store signature separately
604
+ const signature = await MajikSignature.sign(fileBytes, key, {
605
+ contentType: 'application/zip',
606
+ });
607
+ const result = MajikSignature.verifyWithKey(fileBytes, signature, key);
608
+ console.log('Verified:', result.valid); // true
618
609
 
619
- storeSignature();
610
+ // Option B: Sign and embed into the file itself
611
+ const fileBlob = new Blob([fileBytes], { type: 'application/zip' });
612
+ const { blob: signedBlob } = await MajikSignature.signFile(fileBlob, key);
620
613
  ```
621
614
 
622
615
  ---
623
616
 
624
- ### Example 5: Verify from Stored Signature
617
+ ### Example 5: Sign a JSON Payload
625
618
  ```typescript
626
619
  import { MajikKey } from '@majikah/majik-key';
627
- import { MajikSignature } from '@majikah/majik-signature';
620
+ import { MajikSignature, CONTENT_TYPES } from '@majikah/majik-signature';
628
621
 
629
- async function verifyStored() {
630
- // Reload a stored key (locked) and a stored signature
631
- const keyJson = JSON.parse(localStorage.getItem('myKey')!);
632
- const key = MajikKey.fromJSON(keyJson);
633
- // key does NOT need to be unlocked for verification
634
-
635
- const content = 'Original content of the certified document.';
636
-
637
- // Option A: From stored JSON
638
- const storedJson = JSON.parse(localStorage.getItem('doc_sig_001')!);
639
- const signatureA = MajikSignature.fromJSON(storedJson);
640
- const resultA = MajikSignature.verifyWithKey(content, signatureA, key);
641
- console.log('✅ JSON verify:', resultA.valid); // true
642
-
643
- // Option B: From stored base64
644
- const storedB64 = localStorage.getItem('doc_sig_001_b64')!;
645
- const signatureB = MajikSignature.deserialize(storedB64);
646
- const resultB = MajikSignature.verifyWithKey(content, signatureB, key);
647
- console.log('✅ Base64 verify:', resultB.valid); // true
648
- }
622
+ const key = await MajikKey.create(mnemonic, 'passphrase', 'API Key');
649
623
 
650
- verifyStored();
624
+ const payload = {
625
+ userId: 'usr_abc123',
626
+ action: 'transfer',
627
+ amount: 1000,
628
+ currency: 'USD',
629
+ nonce: crypto.randomUUID(),
630
+ };
631
+
632
+ // Always sign the canonical string — agree on stringify format
633
+ const content = JSON.stringify(payload);
634
+ const signature = await MajikSignature.sign(content, key, {
635
+ contentType: CONTENT_TYPES.JSON,
636
+ });
637
+
638
+ const response = { data: payload, signature: signature.toJSON() };
639
+
640
+ // On the receiving end
641
+ const result = MajikSignature.verifyWithKey(
642
+ JSON.stringify(response.data),
643
+ response.signature,
644
+ key,
645
+ );
646
+ console.log('Payload verified:', result.valid); // true
651
647
  ```
652
648
 
653
649
  ---
654
650
 
655
- ### Example 6: Verify Using Only Public Keys
651
+ ### Example 6: Extract and Inspect an Embedded Signature
656
652
  ```typescript
657
653
  import { MajikSignature } from '@majikah/majik-signature';
658
- import type { MajikSignerPublicKeys } from '@majikah/majik-signature';
659
654
 
660
- // Scenario: You only have the signer's public keys (from a registry or
661
- // shared contact card) — no MajikKey instance needed.
655
+ // Extract without verifying useful for inspecting provenance metadata
656
+ const sig = await MajikSignature.extractFrom(file);
662
657
 
663
- async function verifyPublicOnly() {
664
- // Public keys received from a trusted source (e.g. a user profile API)
665
- const publicKeys: MajikSignerPublicKeys = {
666
- signerId: 'base64-fingerprint-of-the-signer',
667
- edPublicKey: new Uint8Array(/* 32 bytes */),
668
- mlDsaPublicKey: new Uint8Array(/* 2592 bytes */),
669
- };
670
-
671
- const content = 'Content to verify.';
672
- const storedSig = MajikSignature.fromJSON(/* stored JSON */);
658
+ if (sig) {
659
+ console.log('Signer ID:', sig.signerId);
660
+ console.log('Signed at:', sig.timestamp);
661
+ console.log('Content hash:', sig.contentHash);
662
+ console.log('Content type:', sig.contentType);
663
+ } else {
664
+ console.log('No signature found');
665
+ }
666
+ ```
673
667
 
674
- // Cross-check signerId against the known signer before trusting
675
- if (storedSig.signerId !== publicKeys.signerId) {
676
- console.error('❌ Signer mismatch — signature is not from expected identity');
677
- return;
678
- }
668
+ ---
679
669
 
680
- const result = MajikSignature.verify(content, storedSig, publicKeys);
681
- console.log('✅ Verified:', result.valid);
682
- console.log('Signed at:', result.timestamp);
683
- }
670
+ ### Example 7: Re-sign a File
671
+ ```typescript
672
+ import { MajikSignature } from '@majikah/majik-signature';
684
673
 
685
- verifyPublicOnly();
674
+ // signFile() strips any existing signature before signing — always safe to call
675
+ const { blob: resignedFile } = await MajikSignature.signFile(previouslySignedFile, key);
676
+ // The new signature covers the original content bytes, not the previously signed file
686
677
  ```
687
678
 
688
679
  ---
689
680
 
690
- ### Example 7: Sign Audio or Video Content
681
+ ### Example 8: Verify Using Only Public Keys
691
682
  ```typescript
692
- import { MajikKey } from '@majikah/majik-key';
693
- import { MajikSignature, CONTENT_TYPES } from '@majikah/majik-signature';
683
+ import { MajikSignature } from '@majikah/majik-signature';
684
+ import type { MajikSignerPublicKeys } from '@majikah/majik-signature';
694
685
 
695
- async function signMediaFile(file: File) {
696
- const mnemonic = MajikKey.generateMnemonic();
697
- const key = await MajikKey.create(mnemonic, 'passphrase', 'Artist Key');
686
+ // Public keys received from a trusted source (e.g. a user profile API)
687
+ const publicKeys: MajikSignerPublicKeys = {
688
+ signerId: 'base64-fingerprint-of-the-signer',
689
+ edPublicKey: new Uint8Array(/* 32 bytes */),
690
+ mlDsaPublicKey: new Uint8Array(/* 2592 bytes */),
691
+ };
698
692
 
699
- // Read file bytes works for WAV, MP3, MP4, MOV, PNG, etc.
700
- const arrayBuffer = await file.arrayBuffer();
701
- const fileBytes = new Uint8Array(arrayBuffer);
693
+ // Verify embedded signature without a MajikKey instance
694
+ const result = await MajikSignature.verifyFile(signedFile, publicKeys, {
695
+ expectedSignerId: publicKeys.signerId,
696
+ });
697
+ console.log('Verified:', result.valid);
698
+ ```
702
699
 
703
- const contentType =
704
- file.type || CONTENT_TYPES.BINARY;
700
+ ---
705
701
 
706
- const signature = await MajikSignature.sign(fileBytes, key, { contentType });
702
+ ### Example 9: Serialize and Store a Signature
703
+ ```typescript
704
+ const signature = await MajikSignature.sign(content, key);
707
705
 
708
- console.log('✅ Media file signed');
709
- console.log('File:', file.name);
710
- console.log('Content Hash:', signature.contentHash);
711
- console.log('Content Type:', signature.contentType);
712
- console.log('Signer:', signature.signerId);
706
+ // Store as JSON
707
+ const json = signature.toJSON();
708
+ await db.signatures.insert({ id: docId, sig: json });
713
709
 
714
- // Attach signature as a sidecar JSON alongside the media file
715
- const sigJson = JSON.stringify(signature.toJSON(), null, 2);
716
- const sigBlob = new Blob([sigJson], { type: 'application/json' });
710
+ // Store as base64 (HTTP header, metadata field, etc.)
711
+ const b64 = signature.serialize();
712
+ res.setHeader('X-Majik-Signature', b64);
717
713
 
718
- // e.g. download as "track.wav.sig.json"
719
- return { signature, sigBlob };
720
- }
714
+ // Restore later
715
+ const sigFromJson = MajikSignature.fromJSON(json);
716
+ const sigFromB64 = MajikSignature.deserialize(b64);
721
717
  ```
722
718
 
723
719
  ---
@@ -725,6 +721,7 @@ async function signMediaFile(file: File) {
725
721
  ## Signature Envelope
726
722
 
727
723
  Every `MajikSignature` serializes to the following JSON structure:
724
+
728
725
  ```json
729
726
  {
730
727
  "version": 1,
@@ -760,29 +757,31 @@ The dominant contributor is `mlDsaSignature` (~6 KB base64) and `signerMlDsaPubl
760
757
  - **Forgery resistance (classical)**: Ed25519 provides 128-bit classical security
761
758
  - **Forgery resistance (post-quantum)**: ML-DSA-87 provides NIST Category 5 post-quantum security
762
759
  - **Hybrid downgrade resistance**: Both algorithms must be broken simultaneously to forge — a break in one is not sufficient
760
+ - **Embed integrity**: File embedding always signs original bytes — the embedding container is never part of what's signed
763
761
 
764
762
  ### What is Your Responsibility
765
763
 
766
- - **Signer identity verification**: The library proves content was signed by a specific key. It does not prove who owns that key in the real world. You must maintain the mapping between `signerId` (fingerprint) and a real-world identity through your own means — a user registry, a `MajikContact`, or a `MajikMessageIdentity`.
767
- - **Byte-for-byte content consistency**: The same bytes must be passed to both `sign()` and `verify()`. For strings, both sides must use the same encoding (UTF-8 is always used internally). For JSON, both sides must use the same `JSON.stringify()` output.
768
- - **Key upgrade**: Legacy MajikKey accounts without signing keys must be re-imported via `importFromMnemonicBackup()` before signing. Load with `key.hasSigningKeys` to check.
764
+ - **Signer identity verification**: The library proves content was signed by a specific key. It does not prove who owns that key in the real world. Maintain the mapping between `signerId` (fingerprint) and a real-world identity through your own means.
765
+ - **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.
766
+ - **Key upgrade**: Legacy MajikKey accounts without signing keys must be re-imported via `importFromMnemonicBackup()` before signing. Check with `key.hasSigningKeys`.
769
767
 
770
768
  ### What NOT to Do
771
769
 
772
770
  ❌ **DON'T** trust `extractPublicKeys()` without cross-checking `signerId` against a known trusted source
773
- ❌ **DON'T** sign JSON by passing the object directly — always `JSON.stringify()` first and agree on format
771
+ ❌ **DON'T** sign JSON by passing the object directly — always `JSON.stringify()` first
774
772
  ❌ **DON'T** transform file bytes (compress, transcode, re-encode) between signing and verification
775
- ❌ **DON'T** pass a locked key to `sign()` — call `unlock()` first
773
+ ❌ **DON'T** pass a locked key to `sign()` or `signFile()` — call `unlock()` first
776
774
  ❌ **DON'T** use `contentType` as a security mechanism — it is advisory only and not enforced
775
+ ❌ **DON'T** assume a Tier-2 trailer signature survives re-muxing — use native-metadata formats where durability matters
777
776
 
778
777
  ### What TO Do
779
778
 
780
- ✅ **DO** verify `result.signerId` matches a known trusted fingerprint after calling `verify()`
781
- ✅ **DO** use `verifyWithKey()` when you have the signer's `MajikKey` — it handles key extraction safely
782
- ✅ **DO** lock the key immediately after signing to purge secret keys from memory
783
- ✅ **DO** store signatures as sidecar files (`.sig.json`) alongside content for easy retrieval
779
+ ✅ **DO** verify `result.signerId` matches a known trusted fingerprint after calling `verify()` or `verifyFile()`
780
+ ✅ **DO** use `verifyWithKey()` / `verifyFile(key)` when you have the signer's `MajikKey` — it handles key extraction safely
781
+ ✅ **DO** lock the key immediately after signing `key.lock()` purges secret keys from memory
782
+ ✅ **DO** use `signFile()` for media and documents to keep signature and content together
783
+ ✅ **DO** use `isSigned()` as a fast guard before calling `verifyFile()` in hot paths
784
784
  ✅ **DO** use `CONTENT_TYPES` constants for standard content type labels
785
- ✅ **DO** call `key.lock()` after `sign()` completes — do not keep keys unlocked longer than needed
786
785
 
787
786
  ---
788
787
 
@@ -794,9 +793,7 @@ Seed phrase account library — required peer dependency for signing.
794
793
  ### [Majik Message](https://message.majikah.solutions)
795
794
  Secure messaging platform using Majik Keys and Majik Signatures for identity-bound communication.
796
795
 
797
- [Read Docs](https://majikah.solutions/products/majik-message/docs)
798
-
799
- Also available on [Microsoft Store](https://apps.microsoft.com/detail/9pmjgvzzjspn) for free.
796
+ [Read Docs](https://majikah.solutions/products/majik-message/docs) · [Microsoft Store](https://apps.microsoft.com/detail/9pmjgvzzjspn)
800
797
 
801
798
  ---
802
799