@majikah/majik-signature 0.2.2 → 0.2.4

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.
@@ -59,3 +59,15 @@ export declare const MAJIK_TIMESTAMP_VERSION: 1;
59
59
  export declare const MAJIK_TSA_DOMAIN: "majikah-tsa-v-1:";
60
60
  export declare const MAJIK_NOTARY_VERSION: 1;
61
61
  export declare const MAJIK_NOTARY_MEMO_DOMAIN: "majik-notary-v-1:";
62
+ export declare const MJKSIG_MAGIC: number[];
63
+ export declare const MJKSIG_MAGIC_LEN: number;
64
+ export declare const MJKSIG_VERSION = 1;
65
+ export declare const MJKSIG_SUPPORTED_VERSIONS: readonly [1];
66
+ export declare const MJKSIG_HEADER_LEN: number;
67
+ /**
68
+ * Proposed IANA media type / conventional file extension for this format.
69
+ * Referenced here so both live in one place ahead of registration —
70
+ * update if/when the registration settles on different values.
71
+ */
72
+ export declare const MJKSIG_MEDIA_TYPE: "application/vnd.majikah.mjksig";
73
+ export declare const MJKSIG_FILE_EXTENSION: ".mjksig";
@@ -58,3 +58,22 @@ export const MAJIK_TIMESTAMP_VERSION = 1;
58
58
  export const MAJIK_TSA_DOMAIN = `majikah-tsa-v-${MAJIK_TIMESTAMP_VERSION}:`;
59
59
  export const MAJIK_NOTARY_VERSION = 1;
60
60
  export const MAJIK_NOTARY_MEMO_DOMAIN = `majik-notary-v-${MAJIK_NOTARY_VERSION}:`;
61
+ // ─── MJKSIG binary format ───────────────────────────────────────────────────
62
+ //
63
+ // Layout: [magic(6)][version(1)][reserved(1)][payloadLen(4, BE u32)][payload JSON]
64
+ // Header length is fixed at 12 bytes regardless of version — only the
65
+ // payload shape may change between versions, never the header layout.
66
+ // This lets fromMJKSIG() always locate and validate the header before it
67
+ // needs to know anything about the payload's internal shape.
68
+ export const MJKSIG_MAGIC = [0x4d, 0x4a, 0x4b, 0x53, 0x49, 0x47]; // "MJKSIG"
69
+ export const MJKSIG_MAGIC_LEN = MJKSIG_MAGIC.length;
70
+ export const MJKSIG_VERSION = 0x01;
71
+ export const MJKSIG_SUPPORTED_VERSIONS = [MJKSIG_VERSION];
72
+ export const MJKSIG_HEADER_LEN = MJKSIG_MAGIC_LEN + 1 + 1 + 4; // 12
73
+ /**
74
+ * Proposed IANA media type / conventional file extension for this format.
75
+ * Referenced here so both live in one place ahead of registration —
76
+ * update if/when the registration settles on different values.
77
+ */
78
+ export const MJKSIG_MEDIA_TYPE = "application/vnd.majikah.mjksig";
79
+ export const MJKSIG_FILE_EXTENSION = ".mjksig";
@@ -10,17 +10,19 @@
10
10
  *
11
11
  * majik-signature → majik-embed → majik-signature ✗
12
12
  *
13
- * Instead, operations that need MajikSignature receive it via the
14
- * MajikSignatureStaticAdapter interface — no circular import needed.
13
+ * Operations that need MajikSignature (signing, verifying) receive it via
14
+ * the MajikSignatureStaticAdapter interface — no circular import needed.
15
15
  *
16
- * Multi-sig + allowlist + seal:
17
- * ─────────────────────────────
18
- * Files embed a MultiSigEnvelope (array of per-signer envelopes + optional
19
- * allowlist + optional seal). parseEnvelope() transparently promotes old
20
- * single-sig files all code here always operates on MultiSigEnvelope.
16
+ * MajikSignatureEnvelope, by contrast, is pure/structural (no crypto), so it
17
+ * IS imported directly here — no adapter required for it. All parsing,
18
+ * validation, allowlist enforcement, seal computation, and signatory/issuer
19
+ * resolution now live on that class (core/envelope.ts). This file is
20
+ * reduced to file-format orchestration: read bytes resolve handler
21
+ * extract/strip → delegate to the envelope class → re-embed.
21
22
  */
22
23
  import type { MajikKey } from "@majikah/majik-key";
23
- import type { EmbedOptions, EmbedResult, EnvelopeInfo, ExpectedSigner, ExtractOptions, ExtractResult, MajikSignatureEnvelope, MajikSignatureJSON, MajikSignerPublicKeys, SealInfo, SealVerificationResult, SignatoriesFilter, SignatoriesResult, SignOptions, VerificationResult } from "../../core/types";
24
+ import type { EmbedOptions, EmbedResult, EnvelopeInfo, ExpectedSigner, ExtractOptions, ExtractResult, MajikSignatureEnvelopeJSON, MajikSignatureJSON, MajikSignerPublicKeys, SealInfo, SealVerificationResult, SignatoriesFilter, SignatoriesResult, SignOptions, VerificationResult } from "../../core/types";
25
+ import { MajikSignatureEnvelope } from "../../core/envelope";
24
26
  import { FormatHandlerRegistry } from "./registry";
25
27
  import { MajikChainAnchor } from "../../anchor/types";
26
28
  export interface MajikSignatureAdapter {
@@ -38,7 +40,7 @@ export declare class MajikSignatureEmbed {
38
40
  /**
39
41
  * Embed a pre-computed signature into a file Blob.
40
42
  * Reads any existing envelope, upserts the new signature by signerId,
41
- * and writes the updated MultiSigEnvelope back.
43
+ * and writes the updated envelope back.
42
44
  * Does NOT sign — call signAndEmbed() for sign + embed together.
43
45
  */
44
46
  static embed(file: Blob, signature: MajikSignatureAdapter | MajikSignatureJSON, options?: EmbedOptions): Promise<EmbedResult>;
@@ -46,15 +48,15 @@ export declare class MajikSignatureEmbed {
46
48
  * Sign a file and embed the signature in one call.
47
49
  *
48
50
  * Flow:
49
- * 1. Extract existing MultiSigEnvelope (or start fresh)
50
- * 2. Reject if envelope is sealed
51
- * 3. Enforce allowlist throw MajikSignatureAllowlistError before any crypto
52
- * 4. Strip existing envelope to get clean original bytes
53
- * 5. If first signer and options.expectedSigners provided: compute allowlistHash
54
- * to bind the allowlist into the signing payload
55
- * 6. Sign the clean bytes (allowlistHash included in payload when present)
56
- * 7. Upsert signature into envelope; if establishing allowlist, attach
57
- * allowlist + allowlistSignerId to envelope
51
+ * 1. Read existing envelope (or start fresh)
52
+ * 2. assertCanSign() rejects sealed envelopes and non-allowlisted signers
53
+ * before any cryptographic operation (issuer always bypasses)
54
+ * 3. Strip existing envelope to get clean original bytes
55
+ * 4. Resolve allowlistHash for this signer (establishing / re-signing / none)
56
+ * 5. Sign the clean bytes
57
+ * 6. If establishing an allowlist, attach it BEFORE upserting the signature
58
+ * (withAllowlist() requires zero existing signatures see note below)
59
+ * 7. Upsert signature into envelope
58
60
  * 8. Embed updated envelope back into the file
59
61
  */
60
62
  static signAndEmbed<T extends MajikSignatureAdapter>(file: Blob, key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: EmbedOptions & {
@@ -67,15 +69,13 @@ export declare class MajikSignatureEmbed {
67
69
  }>;
68
70
  /**
69
71
  * 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.
72
+ * Same flow as signAndEmbed(), minus the final embed step.
73
73
  */
74
74
  static signDetached(file: Blob, key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: EmbedOptions & {
75
75
  contentType?: string;
76
76
  timestamp?: string;
77
77
  expectedSigners?: ExpectedSigner[];
78
- existingEnvelope?: MajikSignatureEnvelope;
78
+ existingEnvelope?: MajikSignatureEnvelope | MajikSignatureEnvelopeJSON | Uint8Array | Blob;
79
79
  }, debug?: boolean): Promise<{
80
80
  blob: Blob;
81
81
  envelope: MajikSignatureEnvelope;
@@ -83,15 +83,13 @@ export declare class MajikSignatureEmbed {
83
83
  mimeType: string;
84
84
  }>;
85
85
  /**
86
- * Extract the MultiSigEnvelope from a file.
86
+ * Extract the envelope from a file as a MajikSignatureEnvelope instance.
87
87
  * Returns null if no signature is found.
88
- * Old single-sig files are promoted to MultiSigEnvelope transparently.
89
88
  */
90
89
  static extract(file: Blob, options?: ExtractOptions): Promise<ExtractResult | null>;
91
90
  /**
92
91
  * Verify a file's embedded signatures against public keys.
93
92
  * Returns one VerificationResult per signature in the envelope.
94
- * Old single-sig files return a single-item array.
95
93
  */
96
94
  static verify(file: Blob, publicKeys: MajikSignerPublicKeys, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & {
97
95
  expectedSignerId?: string;
@@ -100,29 +98,19 @@ export declare class MajikSignatureEmbed {
100
98
  expectedSignerId?: string;
101
99
  }, debug?: boolean): Promise<VerificationResult[]>;
102
100
  /**
103
- * Verify a file against a provided, detached MultiSigEnvelope.
104
- * Skips extraction but still strips the file in case it contains an embedded envelope,
101
+ * Verify a file against a provided, detached envelope (instance, blob or JSON).
102
+ * Still strips the file in case it also contains an embedded envelope,
105
103
  * ensuring verification runs against the clean original bytes.
106
104
  */
107
- static verifyDetached(file: Blob, envelope: MajikSignatureEnvelope, publicKeys: MajikSignerPublicKeys, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & {
105
+ static verifyDetached(file: Blob, envelopeInput: MajikSignatureEnvelope | MajikSignatureEnvelopeJSON | Uint8Array | Blob, publicKeys: MajikSignerPublicKeys, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & {
108
106
  expectedSignerId?: string;
109
107
  }, debug?: boolean): Promise<VerificationResult[]>;
110
- static verifyDetachedWithKey(file: Blob, envelope: MajikSignatureEnvelope, key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & {
108
+ static verifyDetachedWithKey(file: Blob, envelopeInput: MajikSignatureEnvelope | MajikSignatureEnvelopeJSON | Uint8Array | Blob, key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & {
111
109
  expectedSignerId?: string;
112
110
  }, debug?: boolean): Promise<VerificationResult[]>;
113
111
  /**
114
112
  * Seal a multi-sig envelope, preventing any further signatures.
115
- *
116
- * Rules:
117
- * - Only the issuer (allowlistSignerId) may seal
118
- * - The envelope must have an allowlist (must be a restricted multi-sig file)
119
- * - The envelope must not already be sealed
120
- * - The key must be unlocked
121
- *
122
- * The seal hash is SHA3-512 of all current signatories + sealTimestamp,
123
- * prefixed with MAJIK_SEAL_DOMAIN. It is stored in the envelope alongside
124
- * the sealTimestamp and sealedBy fields. No new cryptographic signature
125
- * is produced — the seal is a hash-based integrity lock.
113
+ * Issuer-only / already-sealed checks are enforced by envelope.withSeal().
126
114
  */
127
115
  static seal(file: Blob, key: MajikKey, options?: ExtractOptions & {
128
116
  timestamp?: string;
@@ -132,94 +120,41 @@ export declare class MajikSignatureEmbed {
132
120
  handler: string;
133
121
  mimeType: string;
134
122
  }>;
135
- /**
136
- * Verify the seal hash against the current signatories and sealTimestamp.
137
- * Returns invalid if the envelope is not sealed.
138
- * Does NOT verify the individual cryptographic signatures — call verify() for that.
139
- */
140
123
  static verifySeal(file: Blob, options?: ExtractOptions): Promise<SealVerificationResult>;
141
- /**
142
- * Return seal metadata without verifying.
143
- * Returns null if the envelope is not sealed or has no envelope.
144
- */
145
124
  static getSealInfo(file: Blob, options?: ExtractOptions): Promise<SealInfo | null>;
146
- /**
147
- * Returns true if the file has a sealed envelope (structural check only).
148
- * Does not verify the seal hash.
149
- */
150
125
  static isSealed(file: Blob, options?: ExtractOptions): Promise<boolean>;
151
- /**
152
- * Returns true when the file has a restricted multi-sig envelope
153
- * (allowlist present with more than one expected signer).
154
- * Returns false for unsigned files, open-signing files, or single-signer files.
155
- */
156
126
  static isMultiSig(file: Blob, options?: ExtractOptions): Promise<boolean>;
157
- /**
158
- * Check whether a MajikKey is permitted to add a signature to this file.
159
- *
160
- * Returns false (with a reason) when:
161
- * - The file is sealed
162
- * - The file has an allowlist and the key is not on it (all three fields checked)
163
- *
164
- * Returns true when:
165
- * - The file has no envelope (unsigned — anyone may sign)
166
- * - The file has no allowlist (open signing — anyone may sign)
167
- * - The key is on the allowlist
168
- *
169
- * Always requires a full MajikKey — fingerprint-only checks are not supported
170
- * because they cannot verify the public key fields required by the allowlist.
171
- */
172
127
  static canSign(file: Blob, key: MajikKey, options?: ExtractOptions): Promise<{
173
128
  permitted: boolean;
174
129
  reason?: string;
175
130
  }>;
176
- /**
177
- * Core signatories method. Returns all, signed, and pending arrays.
178
- * Pass filter to narrow the return — the filtered array is still returned
179
- * inside the full SignatoriesResult so callers always have the complete picture.
180
- *
181
- * Returns null if the file has no envelope.
182
- */
183
131
  static getSignatories(file: Blob, options?: ExtractOptions, filter?: SignatoriesFilter): Promise<SignatoriesResult | null>;
184
- /**
185
- * Return the issuer (the signer who established the allowlist and controls sealing).
186
- * Returns null for open-signing files or unsigned files.
187
- */
188
132
  static getIssuer(file: Blob, options?: ExtractOptions): Promise<import("../../core/types").SignatoryInfo | null>;
189
- /**
190
- * Return a full summary of the envelope state in a single file read.
191
- * Useful for rendering UI state (badge, status, signatories list) without
192
- * making multiple separate calls.
193
- * Returns null if the file has no envelope.
194
- */
195
133
  static getEnvelopeInfo(file: Blob, options?: ExtractOptions): Promise<EnvelopeInfo | null>;
196
134
  static strip(file: Blob, options?: ExtractOptions): Promise<Blob>;
197
135
  static hasSignature(file: Blob, options?: ExtractOptions): Promise<boolean>;
198
136
  static getAllowlist(file: Blob, options?: ExtractOptions): Promise<ExpectedSigner[] | null>;
199
137
  static readonly registry: FormatHandlerRegistry;
200
138
  static listHandlers(): string[];
201
- /**
202
- * Check whether a file is eligible for chain anchoring.
203
- * Anchoring requires a sealed envelope — permitted = isSealed(file).
204
- * Chain-agnostic: does not know or care which chain the caller intends to use.
205
- */
206
139
  static canAnchor(file: Blob, options?: ExtractOptions): Promise<{
207
140
  permitted: boolean;
208
141
  reason?: string;
209
142
  }>;
210
143
  /**
211
144
  * Embed an already-confirmed chain anchor into the envelope.
212
- * Does NOT talk to any chain purely appends a record it's handed.
213
- *
214
- * Defensive checks (beyond the master plan's minimum):
215
- * - File must be sealed
216
- * - anchor.payload.digest.value must match the envelope's current sealHash —
217
- * catches a caller accidentally embedding an anchor computed against a
218
- * stale seal (e.g. file was re-sealed between notarize() and this call)
219
- * - Upserts by anchor.id rather than blind-pushing, so a duplicate call
220
- * with the same anchor (e.g. retried after a network blip) doesn't
221
- * produce two entries for the same on-chain transaction
145
+ * Sealed check, digest match, and upsert-by-id dedup are all enforced by
146
+ * envelope.withChainAnchor().
222
147
  */
223
148
  static registerChainAnchor(file: Blob, anchor: MajikChainAnchor, options?: ExtractOptions): Promise<EmbedResult>;
224
149
  static getChainAnchors(file: Blob, options?: ExtractOptions): Promise<MajikChainAnchor[]>;
150
+ private static _prepare;
151
+ /** Extract + parse, or a fresh empty envelope when none exists. */
152
+ private static _readEnvelope;
153
+ private static _noSignatureResult;
154
+ /**
155
+ * Shared by verify() and verifyDetached(): filter by expectedSignerId,
156
+ * verify each remaining signature, and stamp the handler name onto each
157
+ * result. Previously duplicated near-verbatim in both methods.
158
+ */
159
+ private static _verifySignatures;
225
160
  }