npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@waaskey/sdk

v0.4.2

Published

WAASKey — embedded, non-custodial MPC wallets for your app. Official TypeScript SDK.

Downloads

1,192

Readme

@waaskey/sdk

Official TypeScript SDK for Waaskey — embedded, non-custodial MPC wallets for your app. Create wallets and sign transactions where the private key is never assembled in one place (2-of-3 threshold ECDSA, no seed phrase).

Early access (v0.2.x). The public API is taking shape and may change before 1.0.0. Pin an exact version.

Install

pnpm add @waaskey/sdk @waaskey/client-wasm

@waaskey/client-wasm is the WASM MPC engine WasmMpcCore loads at runtime — it is an exact-pinned optional peer (the SDK refuses a mismatched version), so install it alongside the SDK for any app that creates wallets or signs. Passkey / step-up flows additionally need the optional peer @simplewebauthn/browser.

Quickstart

import { Waaskey, WasmMpcCore, EncryptedShareStore, loadClientWasm } from '@waaskey/sdk';

// A secret YOU derive per user (session token, passkey PRF output, device secret) —
// it keys the AES-256-GCM sealing of the local key share; never hardcode it.
const sessionSecret = await deriveUserSessionSecret();

const waaskey = new Waaskey({
  apiKey: process.env.WAASKEY_PUBLISHABLE_KEY!,
  baseUrl: process.env.WAASKEY_API_URL!, // required — there is no default
  // Non-custodial: the device runs its half of the ceremony and seals its key share.
  mpc: new WasmMpcCore(loadClientWasm), // dynamic-imports @waaskey/client-wasm
  shareStore: EncryptedShareStore.browser(sessionSecret), // sealed in IndexedDB
});

// Create an MPC wallet — runs the device keygen, seals the share, waits until active.
const wallet = await waaskey.wallets.create({ chain: 'ethereum' });
// → wallet.id, wallet.address, wallet.status === 'active'

// Sign a 32-byte digest (hex; 0x optional)
const signature = await wallet.sign(digestHex);

Reusing an existing wallet: persist wallet.id (wlt_…) next to your user record and load it later with waaskey.wallets.get(id) — the sealed device share is already in the shareStore under that id, so sign/send work as soon as the sealing secret is available again. Forgot the ids? waaskey.wallets.list() pages through the tenant's wallets. On a brand-new device the share is restored via Multi-factor recovery (below), not re-created.

Node.js (no browser)

The same flow works server-side on Node ≥ 22 (global fetch, WebSocket and WebCrypto are built in). Two things differ from the browser: the wasm engine is loaded from disk (Node's fetch can't read file paths), and there is no IndexedDB — pass MemoryKeyValueStore (or your own KeyValueStore, e.g. DB-backed). Generate the Paillier primes before the ceremony with a PrimePool — inline generation takes minutes of single-threaded CPU and can outlive the server party's ceremony timeout:

import { readFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { Waaskey, WasmMpcCore, EncryptedShareStore, MemoryKeyValueStore, PrimePool, generateRecoveryCode } from '@waaskey/sdk';

const require = createRequire(import.meta.url);

// Node wasm loader: read the bytes from the installed package and initialize with them.
async function loadClientWasmNode() {
  const mod = await import('@waaskey/client-wasm');
  const bytes = await readFile(require.resolve('@waaskey/client-wasm/client_wasm_bg.wasm'));
  await mod.default(await WebAssembly.compile(bytes));
  return mod;
}

const mpc = new WasmMpcCore(loadClientWasmNode);
const waaskey = new Waaskey({
  apiKey: process.env.WAASKEY_API_KEY!,
  baseUrl: process.env.WAASKEY_API_URL!,
  mpc,
  shareStore: new EncryptedShareStore(new MemoryKeyValueStore(), process.env.SHARE_SECRET!),
  primePool: new PrimePool(mpc),
});

// Off the hot path (boot/idle): pre-generate the primes so create() takes seconds.
await waaskey.wallets.prewarm('ethereum');

const recoveryCode = generateRecoveryCode(); // show it to the user once
const wallet = await waaskey.wallets.create({ chain: 'ethereum' }, { backup: { recoveryCode, totpSecret, email } });
const signature = await wallet.sign(digestHex);

A wallet created this way is non-custodial with the server you run this on acting as the "device" — its share lives in your shareStore. Guard that store accordingly.

Send a transaction — WaaS signs, you broadcast

WaaS is a signing service, not a broadcaster (non-custodial: it never owns the mempool, nonce, or status-tracking). wallet.send(...) builds and MPC-signs the transaction and returns the signed raw tx — you submit it from your own node/provider:

const { signedTx, txHash } = await wallet.send({ chainId: 'evm:1', to, value });
// signedTx → the signed raw tx to broadcast; txHash → a deterministic offline id (not a confirmation)

// Broadcast from YOUR node — either the optional helper…
const { txHash: broadcastHash } = await waaskey.broadcast(signedTx, { rpcUrl: 'https://your-rpc' });
// …or your own submitter / provider (recommended for production — you own tracking & retries).

The broadcast helper is best-effort by design — one eth_sendRawTransaction call, no status polling and no retries. Tracking the tx to confirmation is the integrator's concern.

Custody policy — choose the threshold topology (advanced)

wallets.create({ chain }) defaults to the non-custodial 2-of-3 topology [device, server, user_backup] — the platform holds 1 share < t=2, so it can never sign alone (custodyType: 'shared', isNonCustodial: true). To change the (t, n) threshold or the custody guarantee, pass a custody policy; the SDK validates it client-side and the backend enforces the attested invariant platformShares < t ⟺ non-custodial:

const wallet = await waaskey.wallets.create({
  chain: 'ethereum',
  threshold: 2, // t
  parties: ['device', 'user_backup', 'server'], // n = 3 well-known party roles
  custodyKinds: ['user_device', 'user_backup', 'platform_signer'], // parallel to parties
  custodyType: 'shared', // requested posture — the server refuses (400) on a mismatch
});

// The returned wallet surfaces the attestation so you can display the guarantee:
wallet.threshold; // 2
wallet.parties; // ['device','user_backup','server']
wallet.custodyKinds; // ['user_device','user_backup','platform_signer']
wallet.platformShareCount; // 1
wallet.custodyType; // 'shared'
wallet.isNonCustodial; // true — platform's 1 share alone can't reach t=2

CustodyKinduser_device | user_backup | platform_signer | platform_recovery | external_party; CustodyTypeembedded | shared | self_custody. The standalone isNonCustodial(wallet) helper is also exported.

The device runs whatever t-of-n the create ceremony describes — keygen is not pinned to 2-of-3. Coordinating an interactive t-of-n sign across more than one user-held party (each running its half over the relay) is a separate follow-up; today's wallet.sign(...) covers the device+server quorum.

Read the wallet's signing activity (raw signs + send/sweep signed txs, newest first):

const { items } = await wallet.signatures({ page: 1, limit: 20 });

API

| Method | Description | | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | new Waaskey({ apiKey, baseUrl, mpc, shareStore, fetch? }) | Create a client. mpc + shareStore are required to create wallets. | | waaskey.wallets.create({ chain, label?, threshold?, parties?, custodyKinds?, custodyType? }, options?) | Create (keygen + sealed share). Optional custody policy (default: non-custodial 2-of-3 [device, server, user_backup]). Returns a Wallet. | | waaskey.wallets.list(query?, options?) | List the tenant's wallets, newest first (paginated WalletData rows). | | waaskey.wallets.get(id, options?) | Load an existing wallet. | | wallet.sign(digestHex, options?) | Sign a 32-byte digest with the wallet's MPC key. | | wallet.send(params, options?) | Build + MPC-sign a tx. Returns the signed raw tx (you broadcast). | | wallet.signatures(query?, options?) | The wallet's signing activity (paginated). | | waaskey.broadcast(signedTx, { rpcUrl }) | Optional best-effort submit of a signed tx from your own node. |

create / sign accept { signal } for cancellation; create also takes { waitForActive?, activationTimeoutMs?, pollIntervalMs? }.

Errors are thrown as WaaskeyError with a typed code (e.g. unauthorized, forbidden, validation, device_core_required, keygen_failed, aborted) plus status? and details? — branch on error.code, never on the message text.

Secure share storage

The device key share is the user's half of the key. EncryptedShareStore seals it with AES-256-GCM (key derived from a secret you supply — the user's session, a passkey, or a device secret — never embedded) and persists the ciphertext in IndexedDB (MemoryKeyValueStore for tests/SSR). Wipe everything on logout:

await shareStore.clear();

Passkey-derived secret (recommended)

The strongest source for the sealing secret is a passkey with the WebAuthn PRF extension (PasskeyPrfSecretProvider): the 32-byte secret only materializes after the user touches their authenticator (or passes biometrics) and never sits on disk — malware that steals the IndexedDB ciphertext cannot decrypt it in the background. Needs the optional peer @simplewebauthn/browser.

import { PasskeyPrfSecretProvider, isPrfSupported } from '@waaskey/sdk';

const prf = new PasskeyPrfSecretProvider();

// Onboarding — register a PRF-capable passkey (one authenticator touch).
// Persist credentialId + salt anywhere (not secret); keep `secret` in memory only.
const { credentialId, salt, secret } = await prf.enroll();

// Every later session — unlock with the same passkey (another touch).
const { secret: sessionSecret } = await prf.unlock(credentialId, { salt });

const shareStore = EncryptedShareStore.browser(sessionSecret);

Feature-detect with isPasskeySupported() / isPrfSupported() and fall back to a session-derived secret where PRF is unavailable.

Multi-factor recovery

Back up the device share so a user can recover after losing their device. The share is encrypted client-side with a recovery code (the server only ever stores the opaque ciphertext), and its release is gated behind 3 factors: recovery code, TOTP, and email OTP.

// Enrol — show the returned recoveryCode to the user once (it's the only key).
const { recoveryCode } = await waaskey.recovery.register(wallet.id, {
  share: (await shareStore.get(wallet.id))!,
  totpSecret, // base32 authenticator secret
  email,
});

// Later, on a new device — verify factors, re-key, and restore the share locally.
const { challengeId, requiredFactors } = await waaskey.recovery.challenge(wallet.id);
await waaskey.recovery.recover(wallet.id, {
  challengeId,
  recoveryCode,
  verifications: [
    { type: 'recovery_code', token: recoveryCode },
    { type: 'totp', token: otpFromAuthenticator },
    { type: 'email_otp', token: otpFromEmail },
  ],
});

| Method | Description | | ------------------------------------------ | -------------------------------------------------------------- | | recovery.register(walletId, params) | Encrypt the share + enrol factors. Returns the recoveryCode. | | recovery.getInfo(walletId) | The wallet's registered factors. | | recovery.challenge(walletId) | Start a recovery session (challengeId + required factors). | | recovery.recover(walletId, params) | Verify, re-key, decrypt, and restore the share to the store. | | recovery.retrieveShare(walletId, params) | Verify + decrypt the share without re-keying (read-only). |

Balances (client-side, no backend)

WaaS is non-custodial and the backend does not index chain state — balances are read directly from a chain provider, dApp-style. EVM chains work out of the box via a default public RPC; override per chain (or plug a custom provider for non-EVM):

const waaskey = new Waaskey({
  apiKey,
  chains: { ethereum: { rpcUrl: 'https://your-rpc' } }, // optional; EVM has defaults
});

const eth = await waaskey.balances.getBalance('ethereum', wallet.address!);
// → { raw: 1000000000000000000n, decimals: 18, symbol: 'ETH', formatted: '1' }
// (`symbol` is optional in the type — present for built-in EVM chains, may be
//  undefined for a custom provider that doesn't supply one)

const usdc = await waaskey.balances.getTokenBalance('ethereum', usdcAddress, wallet.address!, { symbol: 'USDC' });

Balances are exact bigint base units plus a formatted decimal string (no float). formatUnits(raw, decimals) is exported for rendering.

Development

pnpm install
pnpm test:unit   # vitest
pnpm types       # tsc --noEmit
pnpm lint
pnpm build       # tsup → dist (esm + cjs + d.ts)

License

MIT © WAASKey