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

@burnedsecret/sdk

v2.1.0

Published

Official zero-knowledge SDK for burnedsecret.com

Readme

@burnedsecret/sdk — Official TypeScript/JavaScript SDK for burnedsecret.com

When you put a secret into burnedsecret, your browser (or your client library) encrypts it on your own device before anything leaves you. The encryption key never goes to our server. We store an opaque blob of encrypted bytes; we cannot read it, and neither can anyone who breaches our database, subpoenas us, or gets a court order against us. We have nothing to hand over.

This package is the official JavaScript client. All crypto runs in your process — the server only sees ciphertext, IVs, and (for the request flow) RSA-OAEP wrapped keys. The decryption material lives in your code (URL fragments, or PKCS8 bytes you stash in a KMS), never on our servers.

Works in Node 20+ and all modern browsers. Uses the platform WebCrypto API (globalThis.crypto.subtle) under the hood — no external native deps, no WebAssembly. Ships as a dual ESM + CJS bundle with full TypeScript declarations.

Installation

npm install @burnedsecret/sdk

Node 20 or newer is required for native globalThis.crypto.subtle support. Browsers expose the same API natively.

Quick start

One-way secret (you share a password with someone)

import { BurnedSecret } from "@burnedsecret/sdk";

const bs = new BurnedSecret({ apiKey: "bs_..." });

const { url, id } = await bs.createSecret("hunter2", { ttl: 86400 });
// url looks like: https://burnedsecret.com/s/<id>#k=<base64url AES key>
// The part after '#' never leaves the caller's process — browsers do not
// transmit URL fragments to servers, and this SDK only sends ciphertext + IV.

const plaintext = await bs.readSecret(url);   // burns the secret on read

Secret request (you ask someone to send you a password)

const { url, id, privateKey } = await bs.createRequest({
  prompt: "Send me your AWS root key",
  webhookUrl: "https://acme.example/hooks/bs",
  ttl: 86400,
});
// Persist `privateKey` (PKCS8 DER bytes as an ArrayBuffer) — without it the
// fulfillment is unrecoverable. Use BurnedSecret.exportPrivateKey(privateKey)
// → string for KMS storage.

// Later, after the fulfiller has submitted their answer:
const plaintext = await bs.readFulfillment(id, privateKey);

When the request was created with custom fields, readFulfillment returns an object mapping field name → value instead of a string:

const { url, id, privateKey } = await bs.createRequest({
  prompt: "Send me your AWS keypair",
  fields: [
    { name: "access_key_id", type: "text", required: true },
    { name: "secret_access_key", type: "password", required: true },
  ],
});
const data = await bs.readFulfillment(id, privateKey);
// data === { access_key_id: "AKIA...", secret_access_key: "wJal..." }

Storing private keys in your KMS

// At request-creation time:
const { url, id, privateKey } = await bs.createRequest({ prompt: "..." });
const kmsBlob = BurnedSecret.exportPrivateKey(privateKey); // string (base64url)
await myKms.put(`bs/req/${id}`, kmsBlob);

// At fulfillment-retrieval time:
const kmsBlob2 = await myKms.get(`bs/req/${id}`);
const priv = BurnedSecret.importPrivateKey(kmsBlob2);
const plaintext = await bs.readFulfillment(id, priv);

Verifying webhooks

When a request is fulfilled (or a secret is burned) burnedsecret POSTs a flat JSON event to your webhookUrl, signed with HMAC-SHA256 over the exact body bytes. Fetch your signing secret once from the dashboard API (GET /v1/keys/webhook-secret, Firebase-session auth) and keep it server-side.

import express from "express";
import { BurnedSecret, verifyWebhook, WebhookSignatureError } from "@burnedsecret/sdk";

const bs = new BurnedSecret({ apiKey: process.env.BURNEDSECRET_API_KEY! });

app.post(
  "/webhooks/burnedsecret",
  express.raw({ type: "application/json" }),     // RAW body — never re-serialised JSON
  async (req, res) => {
    let event;
    try {
      event = await verifyWebhook(req.body, req.header("X-Signature"), WEBHOOK_SIGNING_SECRET);
    } catch (e) {
      if (e instanceof WebhookSignatureError) return res.status(401).end();
      throw e;
    }
    // X-Webhook-ID is unique per delivery: store it and ignore repeats (idempotency).
    if (event.event === "request.fulfilled") {
      const priv = BurnedSecret.importPrivateKey(await myKms.get(`bs/req/${event.request_id}`));
      const data = await bs.readFulfillment(event.request_id, priv); // burn-on-read: exactly once
    }
    res.status(204).end();
  },
);

Payloads never contain plaintext, keys, or URLs with key fragments:

{"event": "request.fulfilled", "request_id": "...", "fulfilled_at": "2026-09-14T10:00:00Z"}
{"event": "secret.burned",     "secret_id": "...",  "burned_at":    "2026-09-14T10:00:00Z"}

signWebhookBody(body, secret) produces the same header the server sends, for building fixtures in your own tests. Deliveries are dispatched by a scheduled processor and retried with backoff, so expect minutes rather than seconds between fulfilment and delivery.

Browser usage and partner credentials

The ESM bundle can be consumed by browser bundlers. Never embed a partner API key in browser JavaScript or a public build-time environment variable: visitors can extract it. For partner integrations, run the authenticated SDK on your backend and protect your own application endpoints with authentication and appropriate authorization.

// Backend Node.js only; BURNEDSECRET_API_KEY is a server-side environment value.
import { BurnedSecret } from "@burnedsecret/sdk";

const apiKey = process.env.BURNEDSECRET_API_KEY;
if (!apiKey) throw new Error("BURNEDSECRET_API_KEY is required");
const bs = new BurnedSecret({ apiKey });

The SDK encrypts in that backend process before contacting BurnedSecret. Your backend therefore handles plaintext; BurnedSecret receives only the encrypted envelope. Public recipients use the returned BurnedSecret share/request links. Node CommonJS callers use require("@burnedsecret/sdk").

Public-key handling

Your decryption material never goes to our servers. For requests, the SDK generates an RSA-4096 keypair locally and only sends the public key (SPKI DER, base64url). The private key is returned to you as a PKCS8 DER ArrayBuffer — persist it in your KMS or secret store. If you lose it, the fulfillment is unrecoverable. That's the whole point.

The SDK never derives or stores key material outside of the values you receive from createSecret (which returns the URL containing the AES key in its fragment) and createRequest (which returns privateKey). Anything your process needs to keep, the SDK hands you and forgets.

Error handling

The SDK throws a small exception hierarchy:

BurnedSecretError
├── ApiError                  (HTTP-level failure; has .status, .code)
│   ├── NotFoundError         404 — secret/request not found
│   ├── BurnedError           410 — secret or fulfillment already consumed
│   ├── LegacyApiError        410 — pre-Phase-21 document, not API-accessible
│   └── RateLimitError        429 — has .retryAfterSec
├── CryptoError               local AES/RSA failure (bad key, malformed data)
├── PassphraseRequiredError   protected secret read without a passphrase
└── WebhookSignatureError     incoming webhook failed HMAC verification (respond 401)
import {
  BurnedSecret,
  BurnedError,
  NotFoundError,
  RateLimitError,
  CryptoError,
} from "@burnedsecret/sdk";

const bs = new BurnedSecret({ apiKey: "bs_..." });
try {
  const plaintext = await bs.readSecret(url);
} catch (e) {
  if (e instanceof BurnedError) {
    console.log("Secret has already been viewed and burned.");
  } else if (e instanceof NotFoundError) {
    console.log("Secret never existed or expired.");
  } else if (e instanceof RateLimitError) {
    console.log(`Too many requests — retry after ${e.retryAfterSec}s`);
  } else if (e instanceof CryptoError) {
    console.log("The URL fragment is wrong or the ciphertext is corrupted.");
  } else {
    throw e;
  }
}

new BurnedSecret({ apiKey: "..." }) itself throws when the API key is missing or does not start with bs_.

Test vectors and interop

The wire format — AES-256-GCM with a 12-byte IV and a 128-bit appended tag, RSA-OAEP-SHA256 over a 4096-bit modulus, SPKI/PKCS8 DER with base64url no padding — is pinned at https://burnedsecret.com/api/v1/test-vectors.json.

Any third-party JS implementation can prove conformance by round-tripping that file. The same file lives in this repo at web/api/v1/test-vectors.json and powers tests/interop.test.ts, which asserts four invariants:

  1. AES encrypt with secret.aes_key + iv + plaintext is byte-identical to secret.ciphertext.
  2. AES decrypt of secret.ciphertext recovers secret.plaintext_utf8.
  3. RSA-OAEP decrypt of request.wrapped_key with request.private_key_pkcs8 recovers request.content_aes_key.
  4. AES encrypt with the unwrapped key reproduces request.ciphertext.

If your SDK round-trips all four, it is wire-compatible with this one and with the official Python and Flutter clients.

Crypto specification

Full algorithm parameters, key formats, and design rationale: .planning/design/zero-knowledge-architecture.md in this repository.

Development

git clone https://github.com/JensrudJ/burnedsecret
cd burnedsecret/sdk-js
npm install
npm run typecheck   # tsc --noEmit
npm test            # vitest run — 22 cases
npm run build       # tsup → dist/index.{js,cjs,d.ts}

The test suite has 22 cases: 4 interop conformance, 7 crypto round-trips (including a live RSA-4096 keygen — ~3-5 seconds), 7 client wire-contract, and 4 error-mapping. The longest test (RSA-OAEP local roundtrip 4096) has a 15-second timeout in the vitest config but typically completes in under five seconds on modern hardware.

Releases

Codemagic builds and tests both SDKs at the exact release checkout, including cross-SDK round trips and clean consumer checks. JavaScript release tags must match sdk-js-vVERSION and the version in package.json.

For npm, Codemagic emits the tested tarball and a release manifest. GitHub's npm-trusted-stage.yml verifies the source build and artifact, then uses OIDC and npm stage publish when staging is enabled. Joe approves on npm with 2FA. GitHub does not rebuild the package. Codemagic no longer needs NPM_TOKEN.

The first npm publication requires authorized interactive login/2FA with the Codemagic-built tarball because npm cannot stage a new package. The default handoff is download-only until the package and stage-only trusted publisher are configured. See npm release setup and runbook.

Python releases remain on Codemagic with sdk-py-vVERSION matching pyproject.toml and a required PYPI_TOKEN. Untagged dev builds test without publishing. Release tags and actual publication require separate authorization.

License

MIT — see LICENSE in the repository root.

Authenticated envelope formats

File secrets now use BSF2, which authenticates the complete header and each chunk's position. Old BSF1 files are rejected; there is no legacy fallback. This is an intentional format break for previously disposable test data. Python 1.1.0, JavaScript 2.0.0 and the updated web file viewer use this format; JavaScript 1.2.0 does not support it.

Unprotected text remains raw UTF-8, including literal PP: strings. Protected SDK text uses a binary BSP2 marker that cannot collide with UTF-8 plaintext. Collect a passphrase before fetching a protected secret: a missing/wrong passphrase discovered after burn-on-read cannot be recovered by refetching. See the envelope specification for framing and interop vector version 3. Browser text protection is a separate flow; SDK passphrase-protected text still requires an SDK reader.