@majikah/majik-signature 0.2.0 → 0.2.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.
@@ -42,58 +42,58 @@ export class Mp4Handler {
42
42
  async embed(bytes, signatureJson) {
43
43
  const clean = await this.strip(bytes);
44
44
  const majkBox = this._buildBox(MP4_BOX_TYPE, textEncode(signatureJson));
45
- const boxes = this._parseBoxes(clean, 0, clean.length);
45
+ const { boxes, trailing: topTrailing } = this._parseBoxes(clean, 0, clean.length);
46
46
  const moovIdx = boxes.findIndex((b) => b.type === "moov");
47
47
  if (moovIdx < 0) {
48
- // No moov box — use trailer fallback
49
48
  const { appendTrailer } = await import("../utils");
50
49
  return appendTrailer(clean, signatureJson);
51
50
  }
52
51
  const moovBox = boxes[moovIdx];
53
- const moovChildren = this._parseBoxes(moovBox.data, 0, moovBox.data.length);
52
+ const { boxes: moovChildren, trailing: moovTrailing } = this._parseBoxes(moovBox.data, 0, moovBox.data.length);
54
53
  const udtaIdx = moovChildren.findIndex((b) => b.type === "udta");
55
- let newUdta;
54
+ const { boxes: existingUdtaChildren, trailing: udtaTrailing } = udtaIdx >= 0
55
+ ? this._parseBoxes(moovChildren[udtaIdx].data, 0, moovChildren[udtaIdx].data.length)
56
+ : { boxes: [], trailing: new Uint8Array(0) };
57
+ const newUdta = this._buildBox("udta", concatBytes(...existingUdtaChildren.map((b) => b.raw), majkBox, // ← box-aligned, right after real children
58
+ udtaTrailing));
59
+ const newMoovChildren = [];
56
60
  if (udtaIdx >= 0) {
57
- const udtaBox = moovChildren[udtaIdx];
58
- // Append majk to udta
59
- const newUdtaData = concatBytes(udtaBox.data, majkBox);
60
- newUdta = this._buildBox("udta", newUdtaData);
61
+ // Retain the exact original index of the udta box to avoid byte shifting
62
+ for (let i = 0; i < moovChildren.length; i++) {
63
+ newMoovChildren.push(i === udtaIdx ? newUdta : moovChildren[i].raw);
64
+ }
61
65
  }
62
66
  else {
63
- newUdta = this._buildBox("udta", majkBox);
64
- }
65
- // Rebuild moov
66
- const newMoovChildren = [];
67
- for (let i = 0; i < moovChildren.length; i++) {
68
- if (i === udtaIdx)
69
- continue; // skip old udta (we'll add new one)
70
- newMoovChildren.push(moovChildren[i].raw);
67
+ // If it didn't exist previously, it's safe to append it to the end
68
+ for (let i = 0; i < moovChildren.length; i++) {
69
+ newMoovChildren.push(moovChildren[i].raw);
70
+ }
71
+ newMoovChildren.push(newUdta);
71
72
  }
72
- newMoovChildren.push(newUdta);
73
- const newMoov = this._buildBox("moov", concatBytes(...newMoovChildren));
74
- // Rebuild file
73
+ const newMoov = this._buildBox("moov", concatBytes(...newMoovChildren, moovTrailing));
75
74
  const parts = [];
76
75
  for (let i = 0; i < boxes.length; i++) {
77
- if (i === moovIdx)
78
- parts.push(newMoov);
79
- else
80
- parts.push(boxes[i].raw);
76
+ parts.push(i === moovIdx ? newMoov : boxes[i].raw);
81
77
  }
82
- return concatBytes(...parts);
78
+ return concatBytes(...parts, topTrailing);
83
79
  }
84
80
  async extract(bytes) {
85
81
  if (!this.canHandle(bytes))
86
82
  return null;
87
83
  try {
88
- const boxes = this._parseBoxes(bytes, 0, bytes.length);
84
+ const { boxes } = this._parseBoxes(bytes, 0, bytes.length);
89
85
  const moov = boxes.find((b) => b.type === "moov");
90
- if (!moov)
91
- return null;
92
- const moovChildren = this._parseBoxes(moov.data, 0, moov.data.length);
86
+ // Fallback: If no moov box exists, check for a Tier-2 trailer
87
+ if (!moov) {
88
+ const { extractTrailer } = await import("../utils");
89
+ const trailer = extractTrailer(bytes);
90
+ return trailer ? trailer.signatureJson : null;
91
+ }
92
+ const { boxes: moovChildren } = this._parseBoxes(moov.data, 0, moov.data.length);
93
93
  const udta = moovChildren.find((b) => b.type === "udta");
94
94
  if (!udta)
95
95
  return null;
96
- const udtaChildren = this._parseBoxes(udta.data, 0, udta.data.length);
96
+ const { boxes: udtaChildren } = this._parseBoxes(udta.data, 0, udta.data.length);
97
97
  const majk = udtaChildren.find((b) => b.type === MP4_BOX_TYPE);
98
98
  if (!majk)
99
99
  return null;
@@ -107,32 +107,36 @@ export class Mp4Handler {
107
107
  if (!this.canHandle(bytes))
108
108
  return bytes;
109
109
  try {
110
- const boxes = this._parseBoxes(bytes, 0, bytes.length);
110
+ const { boxes, trailing: topTrailing } = this._parseBoxes(bytes, 0, bytes.length);
111
111
  const moovIdx = boxes.findIndex((b) => b.type === "moov");
112
- if (moovIdx < 0)
113
- return bytes;
112
+ // Fallback: If no moov box exists, strip the Tier-2 trailer if present
113
+ if (moovIdx < 0) {
114
+ const { extractTrailer } = await import("../utils");
115
+ const trailer = extractTrailer(bytes);
116
+ return trailer ? trailer.original : bytes;
117
+ }
114
118
  const moovBox = boxes[moovIdx];
115
- const moovChildren = this._parseBoxes(moovBox.data, 0, moovBox.data.length);
119
+ const { boxes: moovChildren, trailing: moovTrailing } = this._parseBoxes(moovBox.data, 0, moovBox.data.length);
116
120
  const udtaIdx = moovChildren.findIndex((b) => b.type === "udta");
117
121
  if (udtaIdx < 0)
118
122
  return bytes;
119
123
  const udtaBox = moovChildren[udtaIdx];
120
- const udtaChildren = this._parseBoxes(udtaBox.data, 0, udtaBox.data.length);
124
+ const { boxes: udtaChildren, trailing: udtaTrailing } = this._parseBoxes(udtaBox.data, 0, udtaBox.data.length);
121
125
  const filteredUdta = udtaChildren.filter((b) => b.type !== MP4_BOX_TYPE);
122
126
  let newMoovChildren;
123
- if (filteredUdta.length === 0) {
124
- // Remove udta entirely
127
+ // Only remove the udta box entirely if it is completely barren of data and trailing bytes
128
+ if (filteredUdta.length === 0 && udtaTrailing.length === 0) {
125
129
  newMoovChildren = moovChildren
126
130
  .filter((_, i) => i !== udtaIdx)
127
131
  .map((b) => b.raw);
128
132
  }
129
133
  else {
130
- const newUdta = this._buildBox("udta", concatBytes(...filteredUdta.map((b) => b.raw)));
134
+ const newUdta = this._buildBox("udta", concatBytes(...filteredUdta.map((b) => b.raw), udtaTrailing));
131
135
  newMoovChildren = moovChildren.map((b, i) => i === udtaIdx ? newUdta : b.raw);
132
136
  }
133
- const newMoov = this._buildBox("moov", concatBytes(...newMoovChildren));
137
+ const newMoov = this._buildBox("moov", concatBytes(...newMoovChildren, moovTrailing));
134
138
  const parts = boxes.map((b, i) => (i === moovIdx ? newMoov : b.raw));
135
- return concatBytes(...parts);
139
+ return concatBytes(...parts, topTrailing);
136
140
  }
137
141
  catch {
138
142
  return bytes;
@@ -146,14 +150,11 @@ export class Mp4Handler {
146
150
  let size = readUint32BE(bytes, offset);
147
151
  const type = textDecode(bytes.slice(offset + 4, offset + 8));
148
152
  if (size === 1) {
149
- // 64-bit extended size (large box)
150
- // Read as two 32-bit values
151
153
  const hi = readUint32BE(bytes, offset + 8);
152
154
  const lo = readUint32BE(bytes, offset + 12);
153
155
  size = hi * 0x100000000 + lo;
154
156
  }
155
157
  else if (size === 0) {
156
- // Box extends to end of file
157
158
  size = end - offset;
158
159
  }
159
160
  if (size < 8 || offset + size > end)
@@ -164,7 +165,10 @@ export class Mp4Handler {
164
165
  boxes.push({ type, data, raw });
165
166
  offset += size;
166
167
  }
167
- return boxes;
168
+ // Anything left over (padding, malformed box, unparseable trailing bytes)
169
+ // — preserve it verbatim instead of silently dropping it.
170
+ const trailing = bytes.slice(offset, end);
171
+ return { boxes, trailing };
168
172
  }
169
173
  _buildBox(type, data) {
170
174
  const size = 8 + data.length;
@@ -20,7 +20,7 @@
20
20
  * single-sig files — all code here always operates on MultiSigEnvelope.
21
21
  */
22
22
  import type { MajikKey } from "@majikah/majik-key";
23
- import type { EmbedOptions, EmbedResult, EnvelopeInfo, ExpectedSigner, ExtractOptions, ExtractResult, MajikSignatureJSON, MajikSignerPublicKeys, SealInfo, SealVerificationResult, SignatoriesFilter, SignatoriesResult, SignOptions, VerificationResult } from "../../core/types";
23
+ import type { EmbedOptions, EmbedResult, EnvelopeInfo, ExpectedSigner, ExtractOptions, ExtractResult, MajikSignatureEnvelope, MajikSignatureJSON, MajikSignerPublicKeys, SealInfo, SealVerificationResult, SignatoriesFilter, SignatoriesResult, SignOptions, VerificationResult } from "../../core/types";
24
24
  import { FormatHandlerRegistry } from "./registry";
25
25
  import { MajikChainAnchor } from "../../anchor/types";
26
26
  export interface MajikSignatureAdapter {
@@ -63,6 +63,24 @@ export declare class MajikSignatureEmbed {
63
63
  expectedSigners?: ExpectedSigner[];
64
64
  }, debug?: boolean): Promise<EmbedResult & {
65
65
  signature: T;
66
+ envelope: MajikSignatureEnvelope;
67
+ }>;
68
+ /**
69
+ * Sign a file and return the envelope detached.
70
+ * Extracts any existing envelope (or uses a provided one), validates constraints,
71
+ * signs the stripped bytes, and returns the updated MultiSigEnvelope.
72
+ * Does NOT embed the envelope back into the file.
73
+ */
74
+ static signDetached(file: Blob, key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: EmbedOptions & {
75
+ contentType?: string;
76
+ timestamp?: string;
77
+ expectedSigners?: ExpectedSigner[];
78
+ existingEnvelope?: MajikSignatureEnvelope;
79
+ }, debug?: boolean): Promise<{
80
+ blob: Blob;
81
+ envelope: MajikSignatureEnvelope;
82
+ handler: string;
83
+ mimeType: string;
66
84
  }>;
67
85
  /**
68
86
  * Extract the MultiSigEnvelope from a file.
@@ -81,6 +99,17 @@ export declare class MajikSignatureEmbed {
81
99
  static verifyWithKey(file: Blob, key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & {
82
100
  expectedSignerId?: string;
83
101
  }, debug?: boolean): Promise<VerificationResult[]>;
102
+ /**
103
+ * Verify a file against a provided, detached MultiSigEnvelope.
104
+ * Skips extraction but still strips the file in case it contains an embedded envelope,
105
+ * ensuring verification runs against the clean original bytes.
106
+ */
107
+ static verifyDetached(file: Blob, envelope: MajikSignatureEnvelope, publicKeys: MajikSignerPublicKeys, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & {
108
+ expectedSignerId?: string;
109
+ }, debug?: boolean): Promise<VerificationResult[]>;
110
+ static verifyDetachedWithKey(file: Blob, envelope: MajikSignatureEnvelope, key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & {
111
+ expectedSignerId?: string;
112
+ }, debug?: boolean): Promise<VerificationResult[]>;
84
113
  /**
85
114
  * Seal a multi-sig envelope, preventing any further signatures.
86
115
  *
@@ -166,7 +166,91 @@ export class MajikSignatureEmbed {
166
166
  // ── Step 8: Embed ──────────────────────────────────────────────────────
167
167
  const resultBytes = await handler.embed(originalBytes, JSON.stringify(nextEnvelope));
168
168
  const blob = bytesToBlob(resultBytes, mimeType);
169
- return { blob, handler: handler.name, mimeType, signature: signature };
169
+ return {
170
+ blob,
171
+ handler: handler.name,
172
+ mimeType,
173
+ signature: signature,
174
+ envelope: nextEnvelope,
175
+ };
176
+ }
177
+ // ── signDetached ───────────────────────────────────────────────────────────
178
+ /**
179
+ * Sign a file and return the envelope detached.
180
+ * Extracts any existing envelope (or uses a provided one), validates constraints,
181
+ * signs the stripped bytes, and returns the updated MultiSigEnvelope.
182
+ * Does NOT embed the envelope back into the file.
183
+ */
184
+ static async signDetached(file, key, MajikSig, options, debug = false) {
185
+ const bytes = await blobToBytes(file);
186
+ const mimeType = options?.mimeType ?? detectMimeType(bytes, file.type);
187
+ const handler = options?.forceFallback
188
+ ? new FallbackHandler()
189
+ : DEFAULT_REGISTRY.resolve(bytes, mimeType);
190
+ // ── Step 1: Resolve the working envelope ───────────────────────────────
191
+ let envelope;
192
+ if (options?.existingEnvelope) {
193
+ envelope = options.existingEnvelope;
194
+ }
195
+ else {
196
+ const existingRaw = await handler.extract(bytes);
197
+ envelope = existingRaw
198
+ ? parseEnvelope(existingRaw)
199
+ : { version: 1, signatures: [] };
200
+ }
201
+ // ── Step 2: Reject sealed envelopes ────────────────────────────────────
202
+ if (envelope.sealHash) {
203
+ throw new MajikSignatureError("Cannot sign a sealed envelope. The issuer has locked this file against further signatures.");
204
+ }
205
+ // ── Step 3: Allowlist enforcement ──────────────────────────────────────────
206
+ const isIssuer = envelope.allowlistSignerId === key.fingerprint;
207
+ if (!isIssuer) {
208
+ const allowlistCheck = checkAllowlist(envelope, key);
209
+ if (!allowlistCheck.permitted) {
210
+ throw new MajikSignatureAllowlistError(`Signer "${key.fingerprint}" is not permitted to sign this file. ` +
211
+ `The file has a signing allowlist established by "${envelope.allowlistSignerId}".`, key.fingerprint);
212
+ }
213
+ }
214
+ // ── Step 4: Get clean original bytes ───────────────────────────────────
215
+ const originalBytes = await handler.strip(bytes);
216
+ if (debug) {
217
+ const recomputedHash = bytesToBase64(hashContent(originalBytes));
218
+ console.log("signDetached — original bytes hash:", recomputedHash);
219
+ }
220
+ // ── Step 5: Compute allowlistHash if establishing or re-signing ─────────
221
+ const isFirstSigner = envelope.signatures.length === 0;
222
+ const establishingAllowlist = isFirstSigner &&
223
+ options?.expectedSigners &&
224
+ options.expectedSigners.length > 0;
225
+ const isIssuerResigning = !isFirstSigner &&
226
+ !!envelope.allowlist &&
227
+ envelope.allowlist.length > 0 &&
228
+ envelope.allowlistSignerId === key.fingerprint;
229
+ const allowlistHashValue = establishingAllowlist
230
+ ? hashAllowlist(options.expectedSigners)
231
+ : isIssuerResigning
232
+ ? hashAllowlist(envelope.allowlist)
233
+ : undefined;
234
+ // ── Step 6: Sign ───────────────────────────────────────────────────────
235
+ const signature = await MajikSig.sign(originalBytes, key, {
236
+ contentType: options?.contentType,
237
+ timestamp: options?.timestamp,
238
+ ...(allowlistHashValue !== undefined
239
+ ? { allowlistHash: allowlistHashValue }
240
+ : {}),
241
+ });
242
+ // ── Step 7: Build updated envelope ────────────────────────────────────
243
+ let nextEnvelope = upsertSignature(envelope, signature.toJSON());
244
+ if (establishingAllowlist) {
245
+ nextEnvelope = {
246
+ ...nextEnvelope,
247
+ allowlist: options.expectedSigners,
248
+ allowlistSignerId: key.fingerprint,
249
+ };
250
+ }
251
+ // ── Step 8: Return DETACHED (No Embedding) ────────────────────────────
252
+ const blob = bytesToBlob(originalBytes, mimeType);
253
+ return { blob, handler: handler.name, mimeType, envelope: nextEnvelope };
170
254
  }
171
255
  // ── extract ────────────────────────────────────────────────────────────────
172
256
  /**
@@ -273,6 +357,73 @@ export class MajikSignatureEmbed {
273
357
  const publicKeys = MajikSig.publicKeysFromMajikKey(key);
274
358
  return MajikSignatureEmbed.verify(file, publicKeys, MajikSig, options, debug);
275
359
  }
360
+ // ── verifyDetached ─────────────────────────────────────────────────────────
361
+ /**
362
+ * Verify a file against a provided, detached MultiSigEnvelope.
363
+ * Skips extraction but still strips the file in case it contains an embedded envelope,
364
+ * ensuring verification runs against the clean original bytes.
365
+ */
366
+ static async verifyDetached(file, envelope, publicKeys, MajikSig, options, debug = false) {
367
+ const bytes = await blobToBytes(file);
368
+ const mimeType = options?.mimeType ?? detectMimeType(bytes, file.type);
369
+ const handler = DEFAULT_REGISTRY.resolve(bytes, mimeType);
370
+ // Strip the file to ensure we verify against clean bytes
371
+ const originalBytes = await handler.strip(bytes);
372
+ if (debug) {
373
+ const recomputedHash = bytesToBase64(hashContent(originalBytes));
374
+ console.log("verifyDetached — original bytes hash:", recomputedHash);
375
+ }
376
+ // ── Allowlist integrity check ──────────────────────────────────────────
377
+ if (envelope.allowlist && envelope.allowlistSignerId) {
378
+ const recomputedAllowlistHash = hashAllowlist(envelope.allowlist);
379
+ const establisherSig = envelope.signatures.find((s) => s.signerId === envelope.allowlistSignerId);
380
+ if (!establisherSig) {
381
+ return [
382
+ {
383
+ valid: false,
384
+ reason: `Allowlist establisher "${envelope.allowlistSignerId}" has no signature in this envelope`,
385
+ timestamp: new Date().toISOString(),
386
+ },
387
+ ];
388
+ }
389
+ if (establisherSig.allowlistHash !== recomputedAllowlistHash) {
390
+ return [
391
+ {
392
+ valid: false,
393
+ reason: "Allowlist integrity check failed — allowlist may have been tampered with",
394
+ timestamp: new Date().toISOString(),
395
+ },
396
+ ];
397
+ }
398
+ }
399
+ // ── Filter by expectedSignerId if provided ─────────────────────────────
400
+ const sigsToVerify = options?.expectedSignerId
401
+ ? envelope.signatures.filter((s) => s.signerId === options.expectedSignerId)
402
+ : envelope.signatures;
403
+ if (sigsToVerify.length === 0) {
404
+ return [
405
+ {
406
+ valid: false,
407
+ reason: options?.expectedSignerId
408
+ ? `No signature found for signerId "${options.expectedSignerId}"`
409
+ : "Envelope contains no signatures",
410
+ timestamp: new Date().toISOString(),
411
+ },
412
+ ];
413
+ }
414
+ // ── Verify each signature ──────────────────────────────────────────────
415
+ const results = [];
416
+ for (const sig of sigsToVerify) {
417
+ const result = MajikSig.verify(originalBytes, sig, publicKeys);
418
+ results.push({ ...result, handler: handler.name });
419
+ }
420
+ return results;
421
+ }
422
+ // ── verifyDetachedWithKey ──────────────────────────────────────────────────
423
+ static async verifyDetachedWithKey(file, envelope, key, MajikSig, options, debug = false) {
424
+ const publicKeys = MajikSig.publicKeysFromMajikKey(key);
425
+ return MajikSignatureEmbed.verifyDetached(file, envelope, publicKeys, MajikSig, options, debug);
426
+ }
276
427
  // ── seal ───────────────────────────────────────────────────────────────────
277
428
  /**
278
429
  * Seal a multi-sig envelope, preventing any further signatures.
@@ -5,6 +5,7 @@
5
5
  import { MajikChainAnchor } from "../anchor/types";
6
6
  import type { ContentType } from "./constants";
7
7
  export type { ContentType };
8
+ export type MajikSignatureEnvelope = MultiSigEnvelope;
8
9
  /**
9
10
  * The serializable per-signer signature envelope.
10
11
  * Everything a verifier needs — no private keys required.
@@ -4,7 +4,8 @@
4
4
  *
5
5
  */
6
6
  import type { MajikKey } from "@majikah/majik-key";
7
- import type { EnvelopeInfo, ExpectedSigner, MajikSignatureJSON, MajikSignerPublicKeys, MajikTimestamp, MajikTSARequest, SealInfo, SealVerificationResult, SignatoriesFilter, SignatoriesResult, SignatoryInfo, SignOptions, VerificationResult } from "./core/types";
7
+ import type { EnvelopeInfo, ExpectedSigner, MajikSignatureEnvelope, MajikSignatureJSON, MajikSignerPublicKeys, MajikTimestamp, MajikTSARequest, SealInfo, SealVerificationResult, SignatoriesFilter, SignatoriesResult, SignatoryInfo, SignOptions, VerificationResult } from "./core/types";
8
+ import { MajikSignatureEmbed } from "./core/embed/majik-embed";
8
9
  import type { ImageVerificationResult, ImageSignOptions, ImageSignatureStub } from "./core/stamp";
9
10
  import { MajikChainAnchor, MajikChainAnchorMemo } from "./anchor/types";
10
11
  /**
@@ -130,12 +131,26 @@ export declare class MajikSignature {
130
131
  timestamp?: string;
131
132
  mimeType?: string;
132
133
  expectedSigners?: ExpectedSigner[];
133
- }): Promise<{
134
- blob: Blob;
135
- signature: MajikSignature;
136
- handler: string;
137
- mimeType: string;
138
- }>;
134
+ }): ReturnType<typeof MajikSignatureEmbed.signAndEmbed<MajikSignature>>;
135
+ /**
136
+ * Sign a file and return the signature envelope detached.
137
+ *
138
+ * Strips the file of any embedded envelopes, incorporates your new signature
139
+ * into the multi-sig structure, but does NOT embed it back.
140
+ * Useful for external verification workflows where payloads and envelopes travel out-of-band.
141
+ *
142
+ * @example
143
+ * const { blob, signature } = await MajikSignature.signFileDetached(file, aliceKey, {
144
+ * existingEnvelope: outOfBandEnvelope // Optionally pass state from an external source
145
+ * });
146
+ */
147
+ static signFileDetached(file: Blob, key: MajikKey, options?: {
148
+ contentType?: string;
149
+ timestamp?: string;
150
+ mimeType?: string;
151
+ expectedSigners?: ExpectedSigner[];
152
+ existingEnvelope?: MajikSignatureEnvelope;
153
+ }): ReturnType<typeof MajikSignatureEmbed.signDetached>;
139
154
  /**
140
155
  * Verify a file's embedded signatures.
141
156
  * Returns one VerificationResult per signer. Old single-sig files return a single-item array.
@@ -145,6 +160,16 @@ export declare class MajikSignature {
145
160
  expectedSignerId?: string;
146
161
  mimeType?: string;
147
162
  }, debug?: boolean): Promise<VerificationResult[]>;
163
+ /**
164
+ * Verify a file against a detached signature envelope.
165
+ * Skips extraction and verifies the stripped file bytes directly against the provided envelope.
166
+ * Returns one VerificationResult per signer.
167
+ * Pass options.expectedSignerId to verify only a specific signer.
168
+ */
169
+ static verifyFileDetached(file: Blob, envelope: MajikSignatureEnvelope, keyOrPublicKeys: MajikKey | MajikSignerPublicKeys, options?: {
170
+ expectedSignerId?: string;
171
+ mimeType?: string;
172
+ }, debug?: boolean): Promise<VerificationResult[]>;
148
173
  /**
149
174
  * Embed this MajikSignature instance into a file.
150
175
  * The signature must cover the original file bytes BEFORE embedding.
@@ -402,7 +402,22 @@ export class MajikSignature {
402
402
  * });
403
403
  */
404
404
  static async signFile(file, key, options) {
405
- return MajikSignatureEmbed.signAndEmbed(file, key, MajikSignature, options);
405
+ return await MajikSignatureEmbed.signAndEmbed(file, key, MajikSignature, options);
406
+ }
407
+ /**
408
+ * Sign a file and return the signature envelope detached.
409
+ *
410
+ * Strips the file of any embedded envelopes, incorporates your new signature
411
+ * into the multi-sig structure, but does NOT embed it back.
412
+ * Useful for external verification workflows where payloads and envelopes travel out-of-band.
413
+ *
414
+ * @example
415
+ * const { blob, signature } = await MajikSignature.signFileDetached(file, aliceKey, {
416
+ * existingEnvelope: outOfBandEnvelope // Optionally pass state from an external source
417
+ * });
418
+ */
419
+ static async signFileDetached(file, key, options) {
420
+ return MajikSignatureEmbed.signDetached(file, key, MajikSignature, options);
406
421
  }
407
422
  /**
408
423
  * Verify a file's embedded signatures.
@@ -415,6 +430,18 @@ export class MajikSignature {
415
430
  }
416
431
  return MajikSignatureEmbed.verify(file, keyOrPublicKeys, MajikSignature, options, debug);
417
432
  }
433
+ /**
434
+ * Verify a file against a detached signature envelope.
435
+ * Skips extraction and verifies the stripped file bytes directly against the provided envelope.
436
+ * Returns one VerificationResult per signer.
437
+ * Pass options.expectedSignerId to verify only a specific signer.
438
+ */
439
+ static async verifyFileDetached(file, envelope, keyOrPublicKeys, options, debug = false) {
440
+ if (MajikSignature._isMajikKey(keyOrPublicKeys)) {
441
+ return MajikSignatureEmbed.verifyDetachedWithKey(file, envelope, keyOrPublicKeys, MajikSignature, options, debug);
442
+ }
443
+ return MajikSignatureEmbed.verifyDetached(file, envelope, keyOrPublicKeys, MajikSignature, options, debug);
444
+ }
418
445
  /**
419
446
  * Embed this MajikSignature instance into a file.
420
447
  * The signature must cover the original file bytes BEFORE embedding.
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.0",
5
+ "version": "0.2.2",
6
6
  "license": "Apache-2.0",
7
7
  "author": "Zelijah",
8
8
  "main": "./dist/index.js",
@@ -51,7 +51,7 @@
51
51
  "test:watch": "vitest"
52
52
  },
53
53
  "dependencies": {
54
- "@majikah/majik-key": "^0.3.2",
54
+ "@majikah/majik-key": "^0.3.3",
55
55
  "@noble/post-quantum": "^0.6.1",
56
56
  "@stablelib/ed25519": "^2.1.0",
57
57
  "@stablelib/sha256": "^2.0.1",