/** * majik-embed.ts — MajikSignatureEmbed * * Universal MajikSignature embedding and extraction for any file format. * * Circular dependency note: * ───────────────────────── * majik-embed lives inside the majik-signature package and cannot import * MajikSignature directly — that would create a circular dependency: * * majik-signature → majik-embed → majik-signature ✗ * * Operations that need MajikSignature (signing, verifying) receive it via * the MajikSignatureStaticAdapter interface — no circular import needed. * * MajikSignatureEnvelope, by contrast, is pure/structural (no crypto), so it * IS imported directly here — no adapter required for it. All parsing, * validation, allowlist enforcement, seal computation, and signatory/issuer * resolution now live on that class (core/envelope.ts). This file is * reduced to file-format orchestration: read bytes → resolve handler → * extract/strip → delegate to the envelope class → re-embed. */ import type { ISODateString, MajikKey } from "@majikah/majik-key"; import type { BatchFileInput, BatchSignOptions, BatchSignResult, BatchVerifyInput, BatchVerifyOptions, BatchVerifySummary, EmbedOptions, EmbedResult, EnvelopeInfo, ExpectedSigner, ExtractOptions, ExtractResult, FileVerifyResult, MajikSignatureEnvelopeJSON, MajikSignatureJSON, MajikSignerPublicKeys, MajikTimestamp, SealInfo, SealVerificationResult, SignatoriesFilter, SignatoriesResult, SignOptions, VerificationResult } from "../../core/types"; import { MajikSignatureEnvelope } from "../../core/envelope"; import { FormatHandlerRegistry } from "./registry"; import { MajikChainAnchor } from "../../anchor/types"; import { MajikSignatureMap } from "../mjksmap"; import { SignatureOrderResult, VerifySignatureOrderOptions } from "../order"; export interface MajikSignatureAdapter { toJSON(): MajikSignatureJSON; /** * Optional — attach a TSA timestamp to this signature. Present because * MajikSignature implements it; declared optional here so any future * adapter that doesn't support TSA still satisfies this interface. */ addTSA?(tsa: MajikTimestamp): void; } export interface MajikSignatureStaticAdapter { sign(content: Uint8Array | string, key: MajikKey, options?: SignOptions & { allowlistHash?: string; }): Promise; verify(content: Uint8Array | string, signature: MajikSignatureAdapter | MajikSignatureJSON, publicKeys: MajikSignerPublicKeys, now?: Date): VerificationResult; publicKeysFromMajikKey(key: MajikKey): MajikSignerPublicKeys; fromJSON(json: MajikSignatureJSON | string): MajikSignatureAdapter; } export declare class MajikSignatureEmbed { /** * Embed a pre-computed signature into a file Blob. * Reads any existing envelope, upserts the new signature by signerId, * and writes the updated envelope back. * Does NOT sign — call signAndEmbed() for sign + embed together. */ static embed(file: Blob, signature: MajikSignatureAdapter | MajikSignatureJSON, options?: EmbedOptions): Promise; /** * Sign a file and embed the signature in one call. * * Flow: * 1. Read existing envelope (or start fresh) * 2. assertCanSign() — rejects sealed envelopes and non-allowlisted signers * before any cryptographic operation (issuer always bypasses) * 3. Strip existing envelope to get clean original bytes * 4. Resolve allowlistHash for this signer (establishing / re-signing / none) * 5. Sign the clean bytes * 6. If establishing an allowlist, attach it BEFORE upserting the signature * (withAllowlist() requires zero existing signatures — see note below) * 7. Upsert signature into envelope * 8. Embed updated envelope back into the file */ static signAndEmbed(file: Blob, key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: EmbedOptions & { contentType?: string; timestamp?: ISODateString; expectedSigners?: ExpectedSigner[]; /** ISO 8601 expiry for this signature. Omit for one that never expires. */ validUntil?: ISODateString; }, debug?: boolean): Promise; /** * Sign a file and return the envelope detached, along with the specific * signature just produced. * * If options.tsa is provided, it's attached to this signer's signature * via addTSA() immediately after signing and before it's upserted into * the envelope — addTSA() itself validates that the TSA's digest matches * this content's hash and that the TSA's own signature verifies, so no * duplicate validation is needed here. If the adapter doesn't support * addTSA (i.e. MajikSig.addTSA is undefined), a TSA option is a hard * error rather than a silent no-op — attaching a timestamp is something * the caller explicitly asked for, so failing to do it must be loud. * * Returns `signature` — the most recent signature produced by this call * (with the TSA attached, if one was provided) — in addition to the full * `envelope`, so callers don't have to re-extract it via * envelope.findSignature(key.fingerprint) themselves. */ static signDetached(file: Blob, key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: EmbedOptions & { contentType?: string; timestamp?: ISODateString; /** ISO 8601 expiry for this signature. Omit for one that never expires. */ validUntil?: ISODateString; expectedSigners?: ExpectedSigner[]; existingEnvelope?: MajikSignatureEnvelope | MajikSignatureEnvelopeJSON | Uint8Array | Blob; tsa?: MajikTimestamp; }, debug?: boolean): Promise<{ blob: Blob; envelope: MajikSignatureEnvelope; signature: T; handler: string; mimeType: string; }>; /** * Sign a batch of files (folder or zip contents) as detached envelopes, * packaged either as one MajikSignatureMap (default) or as separate * .mjksig Blobs per file. * * Reuses signDetached() per file — no duplicated crypto path. The only * new logic here is path-uniqueness validation and result packaging. */ static signBatchDetached(files: BatchFileInput[], key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: BatchSignOptions, debug?: boolean): Promise; /** * Sign one file detached and extract the contentHash this signer produced * for it — needed to populate the map entry without re-hashing separately. */ private static _signOneDetached; /** * Validate the batch before touching any crypto: non-empty, every file has * a non-empty path, and no two files share a path. Duplicate paths would * otherwise silently overwrite each other's map entry via withEntry()'s * replace-on-match semantics — catching it here means the failure is * "your batch has a duplicate path" up front, not a mysteriously missing * entry discovered later. */ private static _assertValidBatch; /** * Extract the envelope from a file as a MajikSignatureEnvelope instance. * Returns null if no signature is found. */ static extract(file: Blob, options?: ExtractOptions): Promise; /** * Verify a file's embedded signatures against public keys. * Returns one VerificationResult per signature in the envelope. */ static verify(file: Blob, publicKeys: MajikSignerPublicKeys, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & { expectedSignerId?: string; now?: Date; }, debug?: boolean): Promise; static verifyWithKey(file: Blob, key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & { expectedSignerId?: string; now?: Date; }, debug?: boolean): Promise; /** * Verify a file against a provided, detached envelope (instance, blob or JSON). * Still strips the file in case it also contains an embedded envelope, * ensuring verification runs against the clean original bytes. */ static verifyDetached(file: Blob, envelopeInput: MajikSignatureEnvelope | MajikSignatureEnvelopeJSON | Uint8Array | Blob, publicKeys: MajikSignerPublicKeys, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & { expectedSignerId?: string; now?: Date; }, // FIX debug?: boolean): Promise; static verifyDetachedWithKey(file: Blob, envelopeInput: MajikSignatureEnvelope | MajikSignatureEnvelopeJSON | Uint8Array | Blob, key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & { expectedSignerId?: string; now?: Date; }, debug?: boolean): Promise; /** * Verify a batch of extracted files against a MajikSignatureMap. * * For each file: resolve against the map (tolerating relocation — a file * moved or renamed after signing is still found and verified by content, * not just by its original path), then run the normal signature * verification via the envelope stored in that entry. Never throws * per-file — every outcome (missing, tampered, relocated-but-valid, * invalid, verified) is reported in the returned array, so a caller can * render a full per-file status table in one pass instead of catching * exceptions. * * Set options.requireAllPresent to escalate a missing file to a thrown * error instead — useful when the caller expects a closed, complete set * (e.g. "this zip must contain everything the map lists"). */ static verifyFilesFromMjksMap(map: MajikSignatureMap, files: BatchVerifyInput[], publicKeys: MajikSignerPublicKeys, MajikSig: MajikSignatureStaticAdapter, options?: BatchVerifyOptions, debug?: boolean): Promise; /** Convenience overload — resolves public keys from a MajikKey. */ static verifyFilesFromMjksMapWithKey(map: MajikSignatureMap, files: BatchVerifyInput[], key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: BatchVerifyOptions, debug?: boolean): Promise; /** * Summarize a batch verification result — one glance at pass/fail counts * without the caller re-deriving it from the array each time. */ static summarizeBatchVerification(results: FileVerifyResult[]): BatchVerifySummary; /** * Verify the chronological signing order of a file's embedded envelope * against an expected sequence of signers. * expectedOrder accepts MajikKey instances and/or ExpectedSigner objects, * mixed freely — normalized internally. */ static verifyFileOrder(file: Blob, expectedOrder: readonly (MajikKey | ExpectedSigner)[], MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & VerifySignatureOrderOptions): Promise; /** * Verify the chronological signing order against a detached envelope * (instance, JSON, MJKSIG bytes, or Blob). */ static verifyDetachedOrder(file: Blob, envelopeInput: MajikSignatureEnvelope | MajikSignatureEnvelopeJSON | Uint8Array | Blob, expectedOrder: readonly (MajikKey | ExpectedSigner)[], MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & VerifySignatureOrderOptions): Promise; /** * Seal a multi-sig envelope, preventing any further signatures. * Issuer-only / already-sealed checks are enforced by envelope.withSeal(). */ static seal(file: Blob, key: MajikKey, options?: ExtractOptions & { timestamp?: string; }): Promise<{ blob: Blob; sealInfo: SealInfo; handler: string; mimeType: string; }>; static verifySeal(file: Blob, options?: ExtractOptions): Promise; static getSealInfo(file: Blob, options?: ExtractOptions): Promise; static isSealed(file: Blob, options?: ExtractOptions): Promise; static isMultiSig(file: Blob, options?: ExtractOptions): Promise; static canSign(file: Blob, key: MajikKey, options?: ExtractOptions): Promise<{ permitted: boolean; reason?: string; }>; static getSignatories(file: Blob, options?: ExtractOptions, filter?: SignatoriesFilter): Promise; static getIssuer(file: Blob, options?: ExtractOptions): Promise; static getEnvelopeInfo(file: Blob, options?: ExtractOptions): Promise; static strip(file: Blob, options?: ExtractOptions): Promise; static hasSignature(file: Blob, options?: ExtractOptions): Promise; static getAllowlist(file: Blob, options?: ExtractOptions): Promise; static readonly registry: FormatHandlerRegistry; static listHandlers(): string[]; static canAnchor(file: Blob, options?: ExtractOptions): Promise<{ permitted: boolean; reason?: string; }>; /** * Embed an already-confirmed chain anchor into the envelope. * Sealed check, digest match, and upsert-by-id dedup are all enforced by * envelope.withChainAnchor(). */ static registerChainAnchor(file: Blob, anchor: MajikChainAnchor, options?: ExtractOptions): Promise; static getChainAnchors(file: Blob, options?: ExtractOptions): Promise; private static _prepare; /** Extract + parse, or a fresh empty envelope when none exists. */ private static _readEnvelope; private static _noSignatureResult; /** * Shared by verify() and verifyDetached(): filter by expectedSignerId, * verify each remaining signature, and stamp the handler name onto each * result. Previously duplicated near-verbatim in both methods. */ private static _verifySignatures; }