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

@codesense/conseal

v0.4.2

Published

Browser-side zero-knowledge cryptography library using SubtleCrypto.

Readme


Install

npm install conseal

Quick start

import { seal, unseal, generateAesKey } from 'conseal'

// Generate a key and encrypt
const key = await generateAesKey()
const plaintext = await file.arrayBuffer()
const { ciphertext, iv } = await seal(key, plaintext)

// Decrypt
const result = await unseal(key, ciphertext, iv)

API

Symmetric encryption (AES-256-GCM)

| Function | Description | |---|---| | seal(key, plaintext, additionalData?) | Encrypts with a random 96-bit IV. Returns { ciphertext, iv }. | | unseal(key, ciphertext, iv, additionalData?) | Decrypts. Throws on tampered data or AAD mismatch. | | generateAesKey(extractable?) | Generates a random AES-256 key. | | importAesKey(raw, extractable?) | Imports raw key bytes as a CryptoKey. |

Passphrase key wrapping (PBKDF2 + AES-KW)

| Function | Description | |---|---| | wrapKey(passphrase, key) | Wraps a CryptoKey with a passphrase. Returns { wrappedKey, salt }. | | unwrapKey(passphrase, wrappedKey, salt) | Unwraps. Throws on wrong passphrase. | | rekey(oldPass, newPass, wrappedKey, salt) | Changes passphrase without re-encrypting data. |

Asymmetric encryption (ECDH P-256)

| Function | Description | |---|---| | sealMessage(recipientPublicKey, plaintext) | Encrypts for a recipient using ephemeral ECDH. | | unsealMessage(privateKey, ciphertext, iv, ephemeralPublicKey) | Decrypts with the recipient's private key. | | generateECDHKeyPair() | Generates a P-256 ECDH key pair. |

Digital signatures (ECDSA P-256)

| Function | Description | |---|---| | sign(privateKey, data) | Signs data with ECDSA-SHA256. | | verify(publicKey, signature, data) | Verifies a signature. Returns true or false. | | generateECDSAKeyPair() | Generates a P-256 ECDSA key pair. |

Envelope encryption (passcode-protected)

| Function | Description | |---|---| | sealEnvelope(plaintext, passcode) | Encrypts for a recipient without a Conseal account. | | unsealEnvelope(envelope, passcode) | Decrypts with the passcode. | | encodeEnvelope(envelope) | Serialises a SealedEnvelope to JSON. | | decodeEnvelope(json) | Deserialises JSON back to a SealedEnvelope. |

Multi-device private communication (Circle)

Establishes a bounded group of trusted devices that all hold the same Account Encryption Key (AEK). The mnemonic is the root of trust — the AEK is always derived from it, so any device can recover the AEK from the mnemonic alone if the passphrase or wrapped key is lost.

Account creation flow:

const mnemonic = generateMnemonic()          // show to user — write it down, never store it
const { wrappedAEK, aekCommitment, deviceId } = await initCircle(mnemonic, passphrase, secretKey)
// store wrappedAEK + aekCommitment server-side

Recovery flow (lost passphrase or wrapped key):

const aek = await recoverWithMnemonic(mnemonic, true)  // re-derive AEK from mnemonic
const { wrappedKey, salt } = await wrapKey(newPassphrase, aek, newSecretKey)
// upload new wrappedKey to server

Four functions cover the full device-registration ceremony:

| Function | Description | |---|---| | initCircle(mnemonic, passphrase, secretKey) | Founding device derives the shared AEK from the mnemonic, wraps it, and returns wrappedAEK, aekCommitment, and deviceId. | | createJoinRequest(deviceMeta?) | New device generates an ephemeral ECDH key pair. Returns the join request payload, the ephemeral private key (memory-only), and a verificationCode to display to the user. | | authorizeJoin(joinRequest, wrappedAEK, passphrase, secretKey) | Trusted device unwraps its AEK and seals it for the new device via ECDH. Rejects requests older than 5 minutes. | | finalizeJoin(sealedAEK, ephemeralPrivateKey, passphrase, secretKey, aekCommitment) | New device unseals the AEK, verifies the commitment, and re-wraps it under its own credentials. Throws on commitment mismatch. | | deriveVerificationCode(ephemeralPublicKey) | Derives a XX-XX-XX hex code from a public key. Both devices must show matching codes before approval to prevent MITM. |

Device initialisation

| Function | Description | |---|---| | init(wrappedKey, salt, passphrase) | Unwraps the AEK and stores it in IndexedDB. | | AEK_KEY_ID | The IndexedDB key id for the AEK ('aek'). |

Mnemonic recovery (BIP-39)

| Function | Description | |---|---| | generateMnemonic() | Generates a 24-word recovery phrase (256 bits of entropy). | | recoverWithMnemonic(mnemonic, extractable?) | Derives the AEK from the mnemonic. Pass extractable: true when passing to initCircle or wrapKey. |

Key serialisation (JWK)

| Function | Description | |---|---| | exportPublicKeyAsJwk(key) | Exports a public CryptoKey to JWK. | | importPublicKeyFromJwk(jwk, algorithm) | Imports a JWK as a CryptoKey ('ECDH' or 'ECDSA'). |

IndexedDB key storage

import { saveCryptoKey, loadCryptoKey, deleteCryptoKey } from 'conseal'

| Function | Description | |---|---| | saveCryptoKey(name, key) | Persists a CryptoKey to IndexedDB. | | loadCryptoKey(name) | Loads a CryptoKey. Returns null if not found. | | deleteCryptoKey(name) | Deletes a CryptoKey. |

Utilities

| Function | Description | |---|---| | toBase64(buf) / fromBase64(b64) | Standard base64 encoding/decoding. | | toBase64Url(buf) / fromBase64Url(b64) | URL-safe base64 (no padding). | | digest(data) | SHA-256 hash. |

Design

  • Zero runtime secrets on the server. All encryption and decryption happens in the browser. The server stores only wrapped keys and ciphertext.
  • SubtleCrypto everywhere. No OpenSSL, no polyfills, no WASM. The only runtime dependency is @scure/bip39 for mnemonic wordlists (bundled into the output).
  • Non-extractable keys. Keys stored in IndexedDB have extractable: false — JavaScript cannot read the raw bytes, only use them for encrypt/decrypt.
  • PBKDF2 at 600,000 iterations. Passphrase-derived keys use SHA-256 with a 128-bit random salt per wrap. Intentionally slow to resist offline brute-force.

Requirements

  • Browser with SubtleCrypto support (all modern browsers)
  • Node.js >= 18 (for testing / SSR with globalThis.crypto)

Development

npm install
npm test           # unit tests (Vitest + happy-dom)
npm run build      # build to dist/

Cross-browser tests

SubtleCrypto behaviour is not identical across engines. The browser suite runs the full test suite in real Chromium, Firefox, and WebKit engines via Playwright.

First-time setup — download browser binaries (~300 MB, one-off):

npx playwright install

Then run:

npm run test:browser

WebKit is the highest-value target: every browser on iOS uses WebKit under the hood regardless of brand, so this provides real Safari/iOS coverage without a device.

Both suites run automatically on CI for every push and pull request to main.

License

Dual-licensed under AGPL-3.0 and a commercial license.