@miyaocat/sdk-browser
v0.3.0
Published
Headless browser SDK for the control-plane API: device key custody (envelope encryption, WebAuthn PRF, mandatory recovery code), member signing-key lifecycle, request stamps, sessions, and signing-request approval.
Readme
@miyaocat/sdk-browser
Headless browser client for Miyao: device key custody, WebAuthn/passkey unlock, request signing, and participation in wallet ceremonies. No UI.
Install
npm install @miyaocat/sdk-browserUsage
import { MiyaoBrowserClient } from "@miyaocat/sdk-browser";
const client = new MiyaoBrowserClient({ baseUrl: "https://api.miyao.io" });
// Sign in with the session your app obtains from Miyao, then use the typed
// member-facing API (wallets, signing requests, approvals). Ceremony
// participation needs the device module supplied at onboarding.Device shares are sealed in a vault protected by a recovery code and, optionally, a passkey; no method returns share bytes, by design.
Local device partial
Once a signing session is armed, this SDK produces the device's FROST signature share entirely on-device — no network call, and nothing to wake.
import { armLocalSigning } from "@miyaocat/sdk-browser";
// Unlock the sealed share: the passkey (WebAuthn PRF) path, or the mandatory
// recovery code. Either way you get an opaque handle, never share bytes.
const share = await vault.unlockWithPrf(recordId, webAuthnPrf());
// Arm. Validates the frozen roster, this device's transport identity, the key
// epoch and the expiry, then creates the one-shot FROST nonce.
const armed = armLocalSigning({ crypto, grant, share, identity });
// Deliver the device's round-1 commitments over whatever transport you have.
send(armed.commitmentsEnvelope());
// Produce the partial. Synchronous, local, exactly once.
const partial = armed.producePartial({ walletId, message, peerCommitments });producePartial is synchronous on purpose: a synchronous function cannot
await a network response, so "no round trip" is a property of the shape, not a
promise in prose.
The grant is the authorisation boundary and it is yours. The SDK does not decide policy — it refuses to act outside what it was armed for:
- a partial for a different message or wallet is refused, including a message that merely shares a prefix with the armed one;
- the one-shot nonce is never reusable. The session is marked consumed before the crypto module is called, so a failure does not re-open it;
- a stale key epoch, a foreign roster, or a passed expiry refuse at arm time;
- peer commitments are re-checked for ceremony, round and sender, and a tampered envelope is refused by authentication inside the crypto module.
A partial is not a signature. Nothing in the browser can complete one alone — the counterparty in the other trust domain must contribute and aggregate. There is no reduced-quorum path.
What is and is not stored
Stored, in IndexedDB: the share as ciphertext only (a vault blob whose DEK is wrapped by an Argon2id recovery-code KEK, and optionally by a passkey PRF KEK), the public group key package, structural metadata, and the fixed 32-byte PRF input — which is not a secret.
Never stored, never returned, never logged: share bytes, the DEK or KEK, the
recovery code (surfaced exactly once at seal time), the device transport
private key, or envelope payloads. There is no reconstruct-key entrypoint and
none may be added. An armed session's handles live in module-private
WeakMaps, so JSON.stringify of one yields metadata.
The transport identity is not serializable, so it does not survive a page reload — an armed session cannot be resumed after one. Arm again.
The passkey wrap is an unlock optimisation, never the custody root. Enrollment round-trip verifies it and silently drops it on any failure; the mandatory recovery code always remains sufficient. A PRF unlock failure across passkey syncs is an expected outcome — route the user to the recovery code, never dead-end.
Full contract, verification commands and the measured partial time:
docs/api/DEVICE-PARTIAL.md in the repository.
Versioning
/v1 is additive-only; the previous minor is supported for 90 days after the
next minor ships. Pin an exact version.
Security
This package never exports, uploads or reconstructs a private key. Report security issues to [email protected].
License
Apache-2.0. See LICENSE.
Documentation
https://miyao.io
