/** * core/envelope.ts * * MajikSignatureEnvelope — encapsulated multi-signature envelope. * * Replaces the free-function toolkit in multi-sig.ts with a single class that * owns parsing, validation, upserts, allowlist enforcement, seal computation, * and signatory/issuer resolution. * * Design constraints: * - Pure/structural only. No crypto, no signing, no verifying signatures — * that stays in MajikSignature / MajikSignatureEmbed to avoid the * majik-signature ⇄ majik-embed circular dependency. * - Immutable. Every "with*" method returns a new instance; the receiver * is never mutated. * - MajikSignatureEnvelopeJSON (alias of MultiSigEnvelope, see types.ts) * is the on-the-wire shape. This class is the in-memory, behavior-rich * counterpart — same relationship as MajikSignatureJSON / MajikSignature. */ import type { MajikKey } from "@majikah/majik-key"; import type { EnvelopeInfo, ExpectedSigner, MajikSignatureEnvelopeJSON, MajikSignatureJSON, SealInfo, SealVerificationResult, SignatoriesFilter, SignatoriesResult, SignatoryInfo } from "./types"; import type { MajikChainAnchor } from "../anchor/types"; export type AllowlistCheckResult = { permitted: true; entry: ExpectedSigner | null; } | { permitted: false; entry: null; }; export interface CanSignResult { permitted: boolean; reason?: string; } export interface CanAnchorResult { permitted: boolean; reason?: string; } export declare class MajikSignatureEnvelope { #private; private readonly _version; private readonly _signatures; private readonly _allowlist?; private readonly _allowlistSignerId?; private readonly _sealHash?; private readonly _sealTimestamp?; private readonly _sealedBy?; private readonly _chainAnchors?; private constructor(); get version(): 1; get signatures(): readonly MajikSignatureJSON[]; get allowlist(): readonly ExpectedSigner[] | undefined; get allowlistSignerId(): string | undefined; get sealHash(): string | undefined; get sealTimestamp(): string | undefined; get sealedBy(): string | undefined; get chainAnchors(): readonly MajikChainAnchor[]; isSealed(): boolean; hasAllowlist(): boolean; isFirstSigner(): boolean; /** * True when the envelope has an allowlist naming more than one signer. * False for unsigned, open-signing, or single-signer files. */ isMultiSig(): boolean; hasMultipleSignatories(): boolean; findSignature(signerId: string): MajikSignatureJSON | undefined; /** Fingerprint match only — does not verify key material. Use for quick checks. */ isIssuer(keyOrFingerprint: MajikKey | string): boolean; /** * Check whether a MajikKey is permitted to sign, per the allowlist alone * (does not account for seal status or issuer bypass — use assertCanSign() * or canSign() for the full three-field, seal-aware check). * * All three fields must match: signerId (fingerprint), edPublicKey, mlDsaPublicKey. * Prevents a signer from spoofing allowlist membership with a different key * that happens to share a fingerprint. */ checkAllowlist(key: MajikKey): AllowlistCheckResult; /** * Full signing-eligibility gate: sealed check + issuer bypass + allowlist check. * Throws MajikSignatureError (sealed) or MajikSignatureAllowlistError (not permitted). * * This is the single source of truth for "may this key sign this envelope" — * previously duplicated across signAndEmbed() and signDetached() in majik-embed.ts. */ assertCanSign(key: MajikKey): void; /** Non-throwing counterpart of assertCanSign(), for UI-facing checks. */ canSign(key: MajikKey): CanSignResult; static hashAllowlist(allowlist: readonly ExpectedSigner[]): string; /** SHA-256 hash of this envelope's own allowlist, or undefined if none is set. */ get computedAllowlistHash(): string | undefined; /** * Resolve the allowlistHash a given signer's canonical payload should include — * present when establishing a new allowlist (first signer + expectedSigners * provided), or when the issuer is re-signing an already-established allowlist * (keeps it present in their payload so the integrity check keeps passing). * Undefined in every other case. * * Consolidates logic previously duplicated between signAndEmbed() and * signDetached() in majik-embed.ts. */ resolveAllowlistHashFor(key: MajikKey, expectedSigners?: readonly ExpectedSigner[]): string | undefined; /** * Verify that the allowlist establisher's stored allowlistHash still matches * the current allowlist — catches post-hoc tampering with the allowlist array. * Returns { valid: true } trivially when there is no allowlist. */ verifyAllowlistIntegrity(): { valid: boolean; reason?: string; }; /** * Upsert a signature by signerId. Replaces an existing entry (re-sign) or * appends a new one. Refuses on a sealed envelope — sealing is meant to be * a hard lock, so this is enforced here rather than left to callers. */ withSignature(sig: MajikSignatureJSON): MajikSignatureEnvelope; /** * Establish the allowlist. Only permitted once, on an envelope with no * existing allowlist and no prior signatures (i.e. by the very first signer) — * enforced here rather than silently ignored, so misuse fails loudly. */ withAllowlist(allowlist: readonly ExpectedSigner[], signerId: string): MajikSignatureEnvelope; /** * Seal the envelope, preventing any further signatures. Only the issuer may * seal when an allowlist is present; open-signing envelopes may be sealed * by whoever calls this (matches existing seal() behavior in majik-embed.ts). */ withSeal(sealedBy: string, sealTimestamp?: string): MajikSignatureEnvelope; /** * Embed an already-confirmed chain anchor. Requires the envelope to be * sealed, and the anchor's digest must match the current seal hash — guards * against embedding an anchor computed against a stale seal. Upserts by * anchor.id so a retried call doesn't produce duplicate entries. */ withChainAnchor(anchor: MajikChainAnchor): MajikSignatureEnvelope; private computeSealHash; verifySeal(): SealVerificationResult; getSealInfo(): SealInfo | null; canAnchor(): CanAnchorResult; /** * The issuer — the signer who established the allowlist and controls sealing. * Falls back to the first signer for open-signing envelopes. Null if unsigned. * * Consolidates logic previously duplicated between getIssuer() and * getEnvelopeInfo() in majik-embed.ts. */ resolveIssuer(): SignatoryInfo | null; /** * Core signatories method. Returns all/signed/pending — always the full * shape; `filter` narrows which array is the "requested" one without * dropping the others, matching the original alias methods' contract. * Returns null when there is neither an allowlist nor any signatures. */ getSignatories(filter?: SignatoriesFilter): SignatoriesResult | null; /** * Full summary of envelope state in one call — used to render signing-status * UI without multiple separate lookups. */ getEnvelopeInfo(): EnvelopeInfo; toJSON(): MajikSignatureEnvelopeJSON; serialize(): string; static deserialize(base64: string): MajikSignatureEnvelope; /** * Raw MJKSIG bytes, no Blob wrapper. Exposed as a public escape hatch for * non-browser contexts (Node scripts, tests, direct fs writes) where * wrapping in a Blob just to immediately unwrap it again is pure overhead. * toMJKSIG() is the primary API for anything Blob-facing. */ toMJKSIGBytes(): Uint8Array; /** * Encode this envelope as an MJKSIG file Blob. * Always writes the current MJKSIG_VERSION — encoding an old payload * shape under an old version tag is not supported; old versions only * ever appear when *reading* pre-existing MJKSIG binaries. */ toMJKSIG(): Blob; /** * Decode MJKSIG bytes back into a MajikSignatureEnvelope. * Validates magic bytes, version, and declared payload length before * attempting to parse — a truncated or corrupted buffer fails fast with * a clear reason rather than an obscure JSON.parse error. * * Accepts either a Blob (as produced by toMJKSIG()) or raw Uint8Array * (as produced by toMJKSIGBytes(), or read directly off disk) — mirrors * the same "accept either shape" pattern as from(). Reading a Blob * requires awaiting its bytes, which is why this method is async. */ static fromMJKSIG(input: Blob | Uint8Array): Promise; /** * Cheap structural sniff — checks magic bytes only, does not parse or * validate the payload. For a Blob, slices only the header bytes rather * than reading the whole file, so this stays cheap even on large inputs. */ static isMJKSIG(input: Blob | Uint8Array): Promise; /** * Read just the version byte without parsing the payload. * Returns null if the input isn't MJKSIG-shaped at all. */ static getMJKSIGVersion(input: Blob | Uint8Array): Promise; static empty(): MajikSignatureEnvelope; /** * Parse a raw string or plain object into a MajikSignatureEnvelope. * * Handles three shapes: * 1. NEW — MajikSignatureEnvelopeJSON: { version, signatures: [...], ... } * 2. OLD — bare MajikSignatureJSON (has a string `edSignature`): promoted * silently to a single-item envelope with no allowlist. * 3. Anything else — throws MajikSignatureSerializationError. * * This is the only place that knows about the legacy on-disk shape. */ static fromJSON(json: MajikSignatureEnvelopeJSON | MajikSignatureJSON | string): MajikSignatureEnvelope; /** * Accepts an instance, its JSON shape, or MJKSIG bytes/Blob — normalizes to * an instance. This is the single entry point that lets every downstream * caller (verifyDetached, verifyDetachedWithKey, signDetached's * existingEnvelope option) transparently accept a detached envelope * regardless of which form it arrived in. * * Now async: resolving a Blob input requires awaiting its bytes. Every * existing call site is already inside an async method, so this only * costs an added `await` at each call site — no structural changes. */ static from(input: MajikSignatureEnvelope | MajikSignatureEnvelopeJSON | Uint8Array | Blob): Promise; validate(): void; isValid(): boolean; }