Embedded Agent

API reference

Complete EmbeddedAgent and LoadedKeyShare API — every constructor option, every method, every error. Pair with the tutorials in this section for the full usage.

API reference

The complete public surface of EmbeddedAgent and LoadedKeyShare from @zafeguard/mpc-sdk. Every option and every error is documented here; for narrative + examples see the Quick start, Topology, Presignature pool, Recovery, and Storage pages.


EmbeddedAgent

The factory / orchestrator. Construct one per logical signer-set; reuse across many create() / load() / restore() calls if you want to share defaults.

new EmbeddedAgent(config)

new EmbeddedAgent({
  agents: JsAgentConnection[];
  threshold: number;
  curve?: Curve;
  storage?: { kind: StorageKind; tag?: string };
  bridge?: { kind: BridgeKind; timeoutMs?: number; retryMs?: number };
  presignaturePool?: { autoRefillWhen: number; autoRefillTo: number };
});
FieldTypeRequired?Description
agentsArray<{ baseUrl: string; apiKey: string }>yesCloud agents that share custody with the device. Length ≥ 1. Every baseUrl must be distinct (trailing-slash normalised). Device is party 0; agents[i] is party i+1.
thresholdnumberyesSigning quorum (t in t-of-n). Range 2..=(1 + agents.length).
curveCurve enumnoDKG curve. Defaults to Secp256k1.
storage{ kind, tag? }noHint for downstream storage adapters. Doesn't itself persist; see Storage.
bridge{ kind, timeoutMs?, retryMs? }noHTTPS request settings. Defaults: timeoutMs: 30000, retryMs: 250.
presignaturePool{ autoRefillWhen, autoRefillTo }noOpt-in auto-refill. autoRefillTo must be > autoRefillWhen. See Presignature pool.

Errors:

  • agents is required and must hold at least one entry
  • agents[i].baseUrl is required / agents[i].apiKey is required
  • agents[i].baseUrl duplicates another agent in the topology (...)
  • threshold must be >= 2
  • threshold (X) cannot exceed participants (Y); ...
  • presignaturePool.autoRefillTo (X) must be strictly greater than presignaturePool.autoRefillWhen (Y); ...

signer.create(opts)

signer.create({
  keyId: string;
  recovery: JsRecoveryConfig;
  passphrase: string;                   // REQUIRED — seals exportedBlob
  curve?: Curve;
  policyContextJson?: string;
}): Promise<LoadedKeyShare>

Runs DKG with the configured cohort, seals the post-DKG state with passphrase, and returns a loaded handle. The sealed blob is exposed on the returned handle via the exportedBlob getter — persist it via your HSM-backed storage in one round-trip without a separate loaded.export(passphrase) call.

FieldTypeRequired?Description
keyIdstringyesStable identifier the agent uses to find this share on its side. Reuse on load(). Empty / whitespace rejects.
recoveryJsRecoveryConfigyesThe recovery credential — protects loaded.recoveryBundle and unlocks signer.load(...) on a fresh device. See the two-passphrase pattern below.
passphrasestringyesThe export passphrase — seals the post-DKG state into loaded.exportedBlob for routine signer.load(...) on the same device. Same scrypt + AES-256-GCM construction as loaded.export(). Different from the recovery credential above — see the two-passphrase pattern. Empty value rejects.
curveCurvenoPer-call curve override. Defaults to constructor's curve.
policyContextJsonstringnoFree-form JSON forwarded to the agent's pre_dkg policy webhook. Encoded as a string at the NAPI boundary.

Constraints:

  • Multi-agent topology (agents.length >= 2) requires recovery.kind === 'Noop' — recovery agents are themselves the recovery, so a sealed bundle on top is rejected with "a multi-agent topology ... and a non-Noop recovery clause are mutually exclusive".
  • passphrase must not be empty; rejects with "passphrase must not be empty — the post-DKG state is sealed into the returned handle's exportedBlob with this passphrase".

Two passphrases in create()

create() accepts two distinct secretspassphrase (top-level) and recovery.password (nested, when RecoveryKind.Password is used). They protect different artifacts and unlock different recovery paths. Mixing them up is the most common integration mistake.

passphrase (export passphrase)recovery.password (recovery credential)
Sealsloaded.exportedBlob — share material wrapped at rest.loaded.recoveryBundle — off-device envelope that reconstructs the share on a fresh device.
Unlocks viasigner.load({ keyId, blob, passphrase })signer.load({ keyId, recoveryBundle, recovery: { kind, password } })
When you use itEvery routine restart on the same device.Disaster recovery only — new device, no local blob.
SourceMachine secret tied to the device's trust anchor (HSM-derived key, OS keychain). Users never type or see it.The user themselves — typed passphrase / hardware key / social attestation depending on RecoveryKind. The credential the user must remember years later.
RotationRotate via loaded.export(newPassphrase) at any time — cheap, no cohort round-trip.Rotate via loaded.rotateRecoveryCredential only with user consent.
Loss outcomeLocal blob bricked; fall back to recovery. No share loss.Account-level loss if the recovery clause is the only path back.
const exportPassphrase = await hsm.deriveKey('signer-export', userId);   // machine secret
const userPassphrase   = await promptUserForRecoveryCredential();        // human secret

const loaded = await signer.create({
  keyId,
  recovery: { kind: RecoveryKind.Password, password: userPassphrase },   // future restore
  passphrase: exportPassphrase,                                           // sealed blob NOW
});

You can pass the same string for both, but it conflates two different threats: an attacker who steals the local storage (defeated by passphrase) versus one who phishes the user (defeated by recovery.password). Sharing one secret across both lets either attack defeat the other. See Recovery for the full clause matrix when the user has no remembered credential.

Two artifacts produced by create()

The two passphrases pair with two parallel sealed snapshots that come out of create(). They cover different failure modes and live in different storage.

loaded.exportedBlob (export blob)loaded.recoveryBundle (recovery bundle)
What it containsEncrypted snapshot of the device share + transport keys + cached cohort metadata + presignature pool.Sealed parallel copy of just the device share (no transport state, no pool).
Failure mode it survivesProcess restart / app update / reinstall on the same device.The original device is lost / destroyed / wiped.
Sealed bypassphrase (export passphrase)The recovery credential (recovery.password / Social shards / Custodial RSA pubkey — whatever recovery.kind selects).
Restored withsigner.load({ keyId, blob, passphrase })signer.load({ keyId, recoveryBundle, recovery })
Where to persistDevice-local secure storage — OS keychain, Secure Enclave–wrapped store, encrypted DB on the same device that holds the share.Off-device — cloud backup, paper backup, Shamir-split across contacts, custodian's HSM / KMS. Never the same location as the export blob.
Always produced?Yes — create({passphrase}) and reshare({passphrase}) always seal one.Only when recovery.kind !== Noop AND single-agent cohort. null for Noop or multi-agent topologies.
Frequency of useEvery routine restart — fast, local.Only during disaster recovery — slow, off-device.
Without it, can you still recover?Yes — fall back to the recovery bundle (account-level path).Yes — fall back to the export blob (device-level path).

Lose both → the wallet is unrecoverable. The cohort cannot reconstitute a missing device share without at least one of these two artifacts. The agent never sees either blob.

For multi-agent cohorts the recovery bundle is always null because the live recovery agents themselves are the disaster path — see Recovery.

const loaded = await signer.create({
  keyId,
  recovery: { kind: RecoveryKind.Password, password: userPassphrase },
  passphrase: exportPassphrase,
});

// Two snapshots, two storage locations.
await deviceSecureStorage.put(`signer:${userId}`, loaded.exportedBlob!);   // device-local
if (loaded.recoveryBundle) {
  await cloudBackup.put(`recovery:${userId}`, loaded.recoveryBundle);      // off-device
}

License binding: create() also fetches the primary agent's /node/info (it already does, for the agent's transport identity) and captures the agent's license summary onto the returned handle. Read via the loaded.license getter — useful for compliance surfaces that need to report which licensee provisioned a key. Hydrated handles (via load() / restore()) see loaded.license === null until they make their first agent call.

signer.load(opts)

signer.load({
  keyId: string;
  blob: Buffer;
  passphrase: string;
}): Promise<LoadedKeyShare>

Rehydrate a LoadedKeyShare from a previously-exported blob. Purely local; no network round-trip until the next sign().

FieldTypeRequired?Description
keyIdstringyesMust match the id encoded in blob; mismatch throws "keyId mismatch (caller passed X, blob holds Y)".
blobBufferyesOutput of loaded.export(passphrase).
passphrasestringyesSame passphrase used at export. Wrong passphrase fails the AEAD tag check.

Errors:

  • load: blob too short — blob is < salt(16) + nonce(12) + tag(16) bytes.
  • load: blob authentication failed — wrong passphrase or tampered bytes.
  • load: keyId mismatch (...).

The pool is restored from the blob snapshot; see Presignature pool — survives export/load.

signer.load(opts)

signer.load({
  keyId: string;
  recoveryBundle: Buffer;
  recovery: JsRecoveryConfig;
}): Promise<LoadedKeyShare>

Open a recoveryBundle (from a previous create() with a non-Noop recovery clause) and reconstitute a signer on a new device.

FieldTypeRequired?Description
keyIdstringyesMust match the id encoded in recoveryBundle.
recoveryBundleBufferyesOutput of loaded.recoveryBundle at original create time.
recoveryJsRecoveryConfigyesSame kind + credential used to seal the bundle.

Errors:

  • keyId mismatch (caller passed X, recovery bundle holds Y).
  • AEAD tag failure on credential mismatch.

Looking for emergency / raw-private-key recovery?

signer.load(...) rehydrates the device share so signing can resume via the cohort. It does NOT reconstruct the raw root private key.

To extract the raw root private key off-cluster — for MPC-infrastructure-unreachable scenarios, regulator-mandated extraction, migrating off MPC, or re-minting into a different cohort shape — combine ClusterAgent.exportRecoveryBundle(...) (the agents' shards) with loaded.exportRecoveryShard(...) (the device's shard) into RecoveryBundle.finalize(...) + RecoveryBundle.recover(...) on an air-gapped device. The device's shard counts toward the quorum, so "1 surviving agent + the user's device" is enough to recover a 2-of-3 wallet — no need to wait for the rest of the cohort to come back online.

Full walkthroughs:


LoadedKeyShare

Live signer handle. Returned by create(), load(), restore(), and reshare().

Getters

GetterTypeDescription
keyIdstringThe id passed at create time.
publicKeyBufferUncompressed SEC1 root public key (65 bytes for secp256k1).
curveCurveThe curve this signer was minted against.
thresholdnumberSigning quorum.
participantsnumberCohort size (1 + agent.length).
recoveryBundleBuffer | nullSealed bundle produced at create time when the recovery clause was non-Noop. Null otherwise.
exportedBlobBuffer | nullPost-DKG state sealed with the passphrase supplied to create(). Populated by create() only — null for handles produced by load() / restore() / reshare(). Persist this in your HSM-backed storage right after create() returns.
licenseJsLicenseInfo | nullLicense summary fetched from the cohort's primary agent via /node/info at create() time. Lets the caller confirm the licensee identity this signer is bound to without a separate agent.node.info() round-trip. null on hydrated handles.
presignaturePoolStatsJsPresignaturePoolStats{ available, autoRefillWhen?, autoRefillTo? }.

loaded.signMessage(opts) — the one-call path

loaded.signMessage({
  messageHash: Buffer;
  derivationPath?: number[];
  encoding?: SignatureEncoding;
}): Promise<JsSignature>

The everyday signing helper. Runs loaded.sign() (cohort round-trip for partials) and loaded.finalizeSignature() (local combine) in one call, returning the assembled { r, s, v, der, compactRecovery, hashedMessage } directly. No intermediate partials array to thread through.

const messageHash = Utils.sha256(new TextEncoder().encode('hello world'));
const sig = await loaded.signMessage({ messageHash });
const ok = loaded.verifySignature({ messageHash, signature: sig.compactRecovery });

Use signMessage when the wallet signs and finalises on the same host (the common case). For cross-host workflows — one host collects partials, a different host finalises — use the split loaded.sign() + Scheme.finalizeSignature() path documented below.

Errors: same as loaded.sign() + loaded.finalizeSignature() propagated through.

loaded.sign(opts) — partials only (for cross-host workflows)

loaded.sign({
  messageHash: Buffer;
  derivationPath?: number[];
  encoding?: SignatureEncoding;
}): Promise<JsSignWithPartials>

Threshold-sign a 32-byte digest. If the presignature pool has an entry, sign() pops one and skips the inline presigning round-trip. Otherwise runs presigning inline.

Returns the array of partial signatures the cohort produced{ partials, presigSessionId, keyShareId, publicKeyHex, curve, messageHash, derivationPath }. The caller decides when to finalise:

const result = await loaded.sign({ messageHash, derivationPath });
const sig = await loaded.finalizeSignature({
  messageHash: result.messageHash,
  partials: result.partials,
  derivationPath: result.derivationPath,
});
const ok = loaded.verifySignature({
  messageHash,
  signature: sig.compactRecovery,
  derivationPath,
});

For cross-host workflows where the host that runs sign() is not the host that finalises, ship result.partials to the other host and call Scheme.finalizeSignature(messageHash, partials, publicKey) there.

Errors:

  • messageHash must not be empty.
  • party index 1 is reserved for the agent — handle was hydrated with an inconsistent party index (typically only seen on restore() of malformed bundles).
  • Agent-side errors surface as agent <verb> failed: <agent error> with the agent's response inline.

loaded.finalizeSignature(opts)

loaded.finalizeSignature({
  messageHash: Buffer;
  partials: string[];        // base64 PSIG envelopes — from loaded.sign(...).partials
  derivationPath?: number[]; // same path used at sign time, when non-empty
}): Promise<JsSignature>

Combine threshold-many partial signatures into the canonical (r, s, v) tuple using this handle's public key and curve. Thin convenience around Scheme.finalizeSignature(messageHash, partials, publicKey) that closes over the handle's identity.

When derivationPath is non-empty, the SDK derives the verification public key internally so the combined signature is bound to the sub-key.

Per-partial validation. finalizeSignature validates every partial before combining — each PSIG envelope's structural integrity is checked, each share's nonce commitment is re-derived from the public-key share, and the combined signature is verified against the wallet's public key. A bad partial (corrupted on the wire, returned by a malicious or compromised cohort member, or simply produced under a different derivation path) causes the whole call to throw rather than silently producing an invalid signature that would later fail on-chain. Pass skipValidation: true on the lower-level Scheme.finalizeSignature to bypass the check — but never on production paths; the validation is what catches "agent returned junk" before you propagate an invalid signature.

Errors: every per-partial error names the offending entry's index so callers can correlate against the agent that produced it:

  • messageHash must not be empty.
  • partials must not be empty.
  • partial[N] base64 decode: ... — the entry at index N isn't valid base64.
  • partial[N] envelope decode: ... — entry N is base64 but isn't a valid PSIG envelope (truncated, corrupted, or produced by a different scheme version).
  • partial[N] envelope curve mismatch: payload=X, scheme=Y — entry N was signed under a different curve than this handle's curve.
  • Cryptographic validation failures (a structurally-valid partial that doesn't combine into a verifying signature) throw from the underlying combiner with the verification failure surfaced verbatim — the per-partial index isn't named at that depth, but skipValidation: true on the lower-level Scheme.finalizeSignature lets you bypass the cryptographic check if you want to inspect partials yourself. Never skip on production paths.

loaded.verifySignature(opts)

loaded.verifySignature({
  messageHash: Buffer;
  signature: Buffer;          // CompactRecovery form (65 bytes: r || s || v)
  derivationPath?: number[];  // verify against the derived sub-key when non-empty
  format?: SignatureEncoding; // reserved for future expansion; currently ignored
  recoveryId?: number;        // reserved for future expansion; currently ignored
}): boolean

Verify a finalised signature against this handle's public key (or the derived sub-key when derivationPath is non-empty). Synchronous — no network call. Returns true on valid, false on invalid; throws on malformed inputs.

For signatures in non-canonical encodings (DER, compact-without-recovery), use Scheme.verifySignature(...) directly which accepts a format argument.

loaded.mintPresignatures(opts)

loaded.mintPresignatures({ count: number }): Promise<{
  minted: number;
  available: number;
}>

Run count presigning ceremonies in parallel against the agent and append the results to the in-memory pool. All-or-nothing: partial failure throws and leaves the pool unchanged. See Presignature pool.

Errors:

  • count must be >= 1.
  • party index 1 is reserved for the agent.

loaded.listPresignatures() / loaded.presignature.list()

loaded.listPresignatures(): JsPresignaturePublicEntry[]
// — or via the namespace —
loaded.presignature.list(): JsPresignaturePublicEntry[]

Returns the public details of every entry currently in this handle's pool. Each entry: { presignatureId, curve, threshold, maxShares }. No secret share or nonce material is exposed.

The presignatureId is the single canonical handle for a pool entry — the same id appears on this handle's pool, on each cohort agent's /presignatures listing, and on the cluster's audit log. Use it for:

  • Pool-entry selection — pick which entry to consume on the next call to loaded.presignature.sign.
  • Cohort reconciliation — cross-check the device's pool against the cluster's view by intersecting this list with cluster.listPresignatures(). Discrepancies (an id present on the device but missing on the cluster, or vice versa) signal a stale local pool or a cluster-side garbage collection event.
  • Coordinator binding — the same id flows through agent.presignature.sign(presigSessionId, req) so every cohort partial commits to the same nonce as the device's offline partial.
  • Audit-log correlation — emit presignatureId alongside each sign event in your SIEM. Operators can replay a signature end-to-end from the message digest through the chosen pool entry to the cohort partials it combined.

The list reflects the in-memory pool at the moment of the call — it does not roundtrip the cluster. Refresh by calling again after any mint, sign, or pool import operation.

loaded.signPartialOffline(opts) / loaded.presignature.sign(opts)

loaded.signPartialOffline({
  presignatureId: string;       // REQUIRED — caller-named pool entry
  messageHash: Buffer;
  derivationPath?: number[];
}): Promise<JsOfflinePartialSignature>

// — or via the namespace —
loaded.presignature.sign(opts): Promise<JsOfflinePartialSignature>

Produces this device's partial signature against a specific pool-cached presignature, purely locally. No agent round-trip.

The caller MUST pass presignatureId identifying which pool entry to consume. Look up available ids via loaded.presignature.list(). The SDK does not auto-select because the same id must match the cluster's presignature record when the coordinator later collects the cohort's matching partials.

Use this for "turn warm into cold" flows: mint a batch of presignatures while the device is online, disconnect, then produce partial signatures on demand without any cohort connectivity. The returned partial signature is one of threshold-many partials needed to assemble a final signature; pair with a coordinator that:

  1. Receives partialSignatureB64 from this device.
  2. Calls agent.presignature.sign(presigSessionId, req) against each cohort agent to collect their partials.
  3. Calls Scheme.finalizeSignature(messageHash, [device, ...agentPartials], publicKey) to assemble the final signature.

Returns { partialSignatureB64, presigSessionId, keyShareId, publicKeyHex, curve }.

Errors:

  • presignatureId must not be empty — call loaded.presignature.list() to discover available ids.
  • presignatureId 'X' is not in this handle's pool — call loaded.presignature.list() to see available ids, or mint a fresh batch.
  • messageHash must not be empty.

loaded.exportPresignaturePool(opts)

loaded.exportPresignaturePool({ passphrase: string }): Promise<Buffer>

Snapshot the pool into a passphrase-sealed blob bound to this signer's identity (keyId + root public key). See Presignature pool — export and import.

Errors:

  • passphrase must not be empty.

loaded.importPresignaturePool(opts)

loaded.importPresignaturePool({
  blob: Buffer;
  passphrase: string;
}): Promise<{
  imported: number;
  skippedAsDuplicate: number;
  available: number;
}>

Decrypt + validate + dedup-merge a pool blob into this handle's pool. Duplicate detection runs by agent_session_id (idempotent — re-importing the same blob is a no-op).

Errors:

  • passphrase must not be empty.
  • blob authentication failed — wrong passphrase, tampered bytes, or not a presignature-pool blob.
  • unsupported blob format 'X' (this build expects 'zg-presig-pool/v1'); ....
  • blob's keyId ('X') does not match this handle's keyId ('Y'); ....
  • cohort fingerprint mismatch — the blob was exported against a different cohort.

loaded.export(passphrase)

loaded.export(passphrase: string): Promise<Buffer>

Re-seal the live signer state into an encrypted blob under a fresh passphrase. Layout: salt(16) || nonce(12) || ciphertext-with-tag. The pool snapshot rides inside the blob.

Note: create() already produces an initial sealed blob via loaded.exportedBlob. Call export() only when you want to re-seal under a different passphrase (e.g. passphrase rotation) or to snapshot a later state (after mint(), after reshare(), etc.) — not to obtain the initial post-DKG blob.

Errors:

  • passphrase must not be empty.

loaded.exportRecoveryShard(opts)

loaded.exportRecoveryShard({
  rsaPublicKeyPem: string;     // RSA PKCS#8 SubjectPublicKeyInfo PEM
  keyShareId: string;          // canonical id shared with agent shards
}): Promise<Buffer>

Produce a RECB-format recovery shard from the device's local share, byte-compatible with the shards emitted by ClusterAgent.exportRecoveryBundle. The device shard combines with agent shards via RecoveryBundle.finalize(...) to satisfy the quorum needed to reconstruct the raw root private key off-cluster.

FieldTypeRequired?Description
rsaPublicKeyPemstringyesRSA PKCS#8 SubjectPublicKeyInfo PEM block. Must be the same bytes you pass to agent.exportRecoveryBundle({rsaPublicKeyPem}) so the device's shard rsa_pubkey_fp_hex matches the agents'. RecoveryBundle.finalize rejects shard sets with mismatched fingerprints.
keyShareIdstringyesCanonical key share id this cohort agrees on. Look up via cluster.keyShares.list() filtering by publicKeyHex === loaded.publicKey.toString('hex'). Must match the keyShareId used for the agent shards — RecoveryBundle.finalize rejects shards whose headers disagree on key_share_id.

Use case. The realistic disaster scenario for a 2-of-3 wallet: "lost all but one agent, the user's device is intact." Without this method the surviving 1 shard is below threshold; with it, device + 1 agent = 2 = threshold satisfied, wallet is recoverable. See Recovery — Device + surviving agent(s).

License binding stripped. The device's license_fingerprint is cleared before sealing, matching the agent endpoint's behaviour. The cold-storage artefact is not license-bound — emergency recovery works after license expiry.

Returns a Buffer containing the RECB envelope: MAGIC(4) | VERSION(1) | header_len(4 BE) | header_json | mpcblob. The cleartext header commits to (key_share_id, public_key_hex, curve, threshold, max_shares, x_index, rsa_pubkey_fp_hex, node_id, issued_at_ms); mpcblob is the RSA-OAEP+AES-256-GCM-sealed JSON-encoded KeyShare.

Errors:

  • rsaPublicKeyPem must not be empty.
  • keyShareId must not be empty — must match the keyShareId used when calling agent.exportRecoveryBundle against the surviving cohort agents.

loaded.rotateRecoveryCredential(opts)

loaded.rotateRecoveryCredential({
  oldRecovery: JsRecoveryConfig;
  newRecovery: JsRecoveryConfig;
}): Promise<Buffer>

Re-seal the recovery bundle under a fresh credential. Verifies oldRecovery against the current bundle before re-sealing. Returns the new bundle.

This is a planned-rotation tool, not a forgotten-credential recovery tool. The caller MUST still possess oldRecovery. Use cases: hygiene rotation of the bundle passphrase, migrating between recovery kinds (PasswordSocial), custodian pre-rotation of HSM keypair, Social distribution-list change while still operational.

If the OLD credential is genuinely lost, this method cannot help — use signer.load({ blob, passphrase }) against the persisted export blob and then loaded.reshare({ recovery, passphrase }) to mint a fresh bundle under a new credential.

Errors:

  • oldRecovery does not match the credential the current bundle was sealed with.

loaded.reshare(opts)

loaded.reshare({
  recovery?: JsRecoveryConfig;
  passphrase: string;                   // REQUIRED — seals reshared.exportedBlob
}): Promise<LoadedKeyShare>

Rotate the cryptographic material across the cohort, preserving the wallet's root public key. Returns a fresh LoadedKeyShare with the post-reshare state already sealed onto reshared.exportedBlob under passphrase — mirrors signer.create()'s one-round-trip ergonomics. The old handle's share is functionally dead once the agent advances.

Works at every supported topology — single-agent 2-of-2, multi-agent threshold-2 (prefix-cohort), and multi-agent threshold-N (relay driver).

FieldTypeRequired?Description
recoveryJsRecoveryConfigconditionalRequired when the source handle's recoveryBundle is non-empty (otherwise the rotated state would invalidate the existing bundle silently). On hydrated handles (via load() or restore()) the recoveryBundle field is null even when the original was sealed — pass recovery explicitly to preserve the bundle through the rotation. The supplied credential always re-seals: if the source had no bundle and you pass a credential, the rotated handle gets a freshly-sealed bundle (opt-in). To rotate the credential ITSELF, run rotateRecoveryCredential() separately after reshare.
passphrasestringyesThe export passphrase — seals the post-reshare state into reshared.exportedBlob for routine signer.load(...) on the same device. Same scrypt + AES-256-GCM construction as loaded.export(). Distinct from recovery.password — see the two-passphrase pattern. Empty value rejects.

See Recovery — Share rotation.

Errors:

  • passphrase must not be empty — the post-reshare state is sealed into the returned handle's exportedBlob with this passphrase.
  • this handle carries a sealed recoveryBundle, so reshare requires the recovery credential — source handle has a bundle but opts.recovery is missing.
  • recovery credential does not open the current recoveryBundle — supplied credential is wrong. Reshare aborts before any share rotation so the existing bundle stays valid.
  • single-agent multi-party (participants=N) reshare driver not implemented in this build — niche path: a single-agent signer constructed with participants > 2. The user-facing JS API doesn't produce this shape today; surfaces only when extending the SDK with a custom topology.

loaded.destroy()

loaded.destroy(): Promise<void>

Proactively wipes every secret byte the handle holds: the device's MPC key share, the Ed25519 and X25519 private identity keys, every per-party state entry, the sealed recovery bundle (if present), and the entire presignature pool. The actual zeroization happens in Rust — KeyShare and PreSignature zero their secret bytes in their own Drop impl; the bare 32-byte private-key arrays and the recovery blob are wiped explicitly. After destroy() returns the handle is tombstoned.

After destroy():

  • loaded.keyId and loaded.publicKey stay readable — both are public material.
  • loaded.recoveryBundle returns null.
  • loaded.presignature.stats.available is 0.
  • sign(), presignature.mint(), presignature.export(), export(), reshare(), and rotateRecoveryCredential() all reject synchronously with signer destroyed — call create() or load() to obtain a fresh handle.
  • A second destroy() is a no-op.

Concurrency. A ceremony that started before destroy() keeps its own reference to the secret material until it finishes — destroy() doesn't yank the share out from under an in-flight sign(). The new sign() that races after destroy() returns sees the tombstone immediately. For deterministic ordering, await every in-flight promise before calling destroy().

The agent-side counter-share is not touched. Call presignature.export() first if you need to preserve the cached pool elsewhere, then destroy().


Supporting types

Curve

enum Curve {
  Secp256k1, Bip340 /* a.k.a. Secp256k1Schnorr */,
  Ed25519, Ed448,
  P256, P384, P521,
  Sr25519, Starknet, Mina, Zilliqa,
}

See MPC SDK — Supported curves for chain mappings.

JsRecoveryConfig

type JsRecoveryConfig =
  | { kind: 'Noop' }
  | { kind: 'Password'; password: string }
  | { kind: 'Custodial'; custodialPublicKeyPem: string }
  | { kind: 'Social'; socialThreshold: number; socialRecipientsHex: string[] };

Validation runs at call time; mismatched kinds + fields throw before any side effect:

  • recovery.password is required for kind: Password.
  • recovery.custodialPublicKeyPem is required for kind: Custodial.
  • recovery.socialThreshold (X) > recipients (Y); cannot reconstitute.

See Recovery — Sealed-bundle cold backup.

JsSignWithPartials

Return shape of loaded.sign. Carries the array of partials the cohort produced plus the metadata needed to finalise off-line:

{
  partials: string[];       // base64 PSIG envelopes — threshold-many entries
  presigSessionId: string;  // cluster-side presig session id for audit-log correlation
  keyShareId: string;       // cluster-side key share id (= dkg session id)
  publicKeyHex: string;     // root pubkey (hex)
  curve: string;            // curve identifier
  messageHash: Buffer;      // echo of the signed digest — for re-feeding into finalize
  derivationPath: number[]; // echo of the path used at sign time (empty = root)
}

Pass directly to loaded.finalizeSignature({ messageHash, partials, derivationPath }) for the convenience path, or to Scheme.finalizeSignature(messageHash, partials, publicKey) from any other host.

JsSignature

Return shape of loaded.finalizeSignature and Scheme.finalizeSignature(...):

{
  r: Buffer;                // 32-byte r component
  s: Buffer;                // 32-byte s component
  v: number;                // recovery byte (0/1 for secp256k1; 0 for non-ECDSA curves)
  der: Buffer;              // ASN.1 DER encoding
  rawRs: Buffer;            // r || s (64 bytes)
  compactRecovery: Buffer;  // r || s || v (65 bytes)
}

Pass compactRecovery to loaded.verifySignature or to Scheme.verifySignature(...) with SignatureFormat.CompactRecovery and the signer's public key.

JsPresignaturePoolStats

{
  available: number;
  autoRefillWhen: number | null;
  autoRefillTo: number | null;
}

available is the current pool depth. autoRefillWhen / autoRefillTo echo the constructor's presignaturePool config (both null when manual-only).

JsPresignaturePublicEntry

{
  presignatureId: string;       // pass to `loaded.presignature.sign` as the selector
  curve: string;
  threshold: number;
  maxShares: number;
}

Public details of one presignature pool entry as returned by loaded.presignature.list(). No secret material exposed.

JsOfflinePartialSignature

{
  partialSignatureB64: string;  // PSIG envelope — feed into Scheme.finalizeSignature
  presigSessionId: string;      // bind the cohort's partials to the same presignature
  keyShareId: string;           // keyShareId for the SignRequest body
  publicKeyHex: string;         // root pubkey — for Scheme.finalizeSignature
  curve: string;
}

Result of loaded.presignature.sign. Pair with the coordinator's calls into agent.presignature.sign plus Scheme.finalizeSignature to assemble the final signature.

JsLicenseInfo

{
  licensee: string;
  email: string;
  expiryUnixSec: number;
  expiryIso: string | null;
  valid: boolean;
  features: string[];
}

License summary exposed via loaded.license and agent.node.info().license. Captured at create() time from the primary agent's /node/info.


See also

On this page