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

@codefusion-cc/sealed

v0.1.3

Published

Secrets kept at rest in a database, sealed with AES-GCM under a key from a Worker secret (each bound to where it is kept, with keys that can be replaced without losing what the old one sealed), and small secrets sealed to a recipient's X25519 public key s

Downloads

1,762

Readme

@codefusion-cc/sealed

Secrets an app keeps in its own database (the API keys of the services it calls, tokens people paste into an admin panel) sealed with AES-256-GCM under a key the database never holds: a Worker secret. A copy or an export of the database gives none of them away. WebCrypto only, so it runs in Workers, Node and Deno; its one dependency is @codefusion-cc/workers-crypto, for base64url and digests.

npm install @codefusion-cc/sealed

@codefusion-cc/sealed/box seals a small secret to someone's public key instead, so that only they open it and whoever relays it cannot (below).

The key

codefusion-console cloudflare workers:edit -- sh -c 'npx -p @codefusion-cc/sealed codefusion-sealed-key | npx wrangler secret put CONNECTIONS_KEY'

The key goes straight into the secret, never onto the screen. Wrangler runs with a token minted for this one command, which codefusion-console deletes when it exits: no wrangler login on the Mac.

In the Worker

import { loadKeyring } from '@codefusion-cc/sealed'

const keyring = await loadKeyring(env.CONNECTIONS_KEY) // null: no key, or the secret is not one
if (!keyring) throw new Error('Set CONNECTIONS_KEY first')

// Bound to where it is kept: the same text opens only for the same context.
const sealed = await keyring.seal(JSON.stringify({ token }), `connection:${id}`)

const opened = await keyring.open(row.sealed, `connection:${row.id}`)
if (opened.ok) {
  use(JSON.parse(opened.value))
  if (opened.stale) await save(row.id, await keyring.seal(opened.value, `connection:${row.id}`))
} else {
  // 'unknown-key': the key that sealed it is gone; 'tampered': changed, or moved from another row;
  // 'malformed': not a sealed value.
  askForItAgain(opened.reason)
}
  • A sealed value is v1.<key id>.<iv>.<ciphertext> (base64url). The key id is the first 8 hex digits of the key's SHA-256: it names the key without giving it away, so an app can show which key its rows need.
  • open never throws for what a row may hold; seal and open throw when given a context that is not a string, which only a programming mistake does, and reject when the platform's WebCrypto fails (as loadKeyring does, which then tries the secret again on its next call). tampered and unknown-key always mean the row, never an outage.
  • open gives back exactly the text that was sealed, a leading byte order mark (U+FEFF) or whitespace included. Clean up what someone pasted before sealing it: value.trim() drops both, so a key copied from a file saved with a BOM still fits in a request header.
  • Keys are imported once per isolate for the same secret text.

A new key

A secret may hold several keys, newest first, apart by commas or whitespace. The newest seals; every key opens, and what an older key sealed opens as stale: true, so the app seals it again as it reads it.

codefusion-console cloudflare workers:edit -- npx wrangler secret put CONNECTIONS_KEY   # paste: <new key>,<old key>

Once every row is sealed again (each open says stale: false), put only the new key in the secret. Without the old key there is no way back: what it sealed must be entered again.

Moving from earlier code

Values written before as <iv>.<ciphertext> (standard base64, no context, as dieselpaul's plugins did) open with open(value, context, { legacy: true }), always as stale: seal each again as it is read. Leave legacy off once none are left, since a value in that format is bound to no row.

Sealed to a public key

@codefusion-cc/sealed/box is for a secret that passes through a server that must not read it: a page seals it to the public key of whoever will use it (a relay on someone's Mac, a device), the server stores or forwards the sealed text, and only the holder of the private key opens it. It works like libsodium's sealed box, on WebCrypto alone, so the same code runs in browsers (Chromium, Firefox, Safari), Node 20 and later, and Workers.

On the recipient, at each start: new keys only when there is no key file yet. New keys over an old file would leave every box sealed to the old public key unopenable, so anything else (a file it cannot read, one that holds no keys, a WebCrypto failure) stops the start instead:

import { link, readFile, rm, writeFile } from 'node:fs/promises'
import { generateBoxKeys, loadBoxKeys, saveBoxKeys, type BoxKeys } from '@codefusion-cc/sealed/box'

async function recipientKeys(path: string): Promise<BoxKeys> {
  let text: string
  try {
    text = await readFile(path, 'utf8')
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error // unreadable is not missing
    const keys = await generateBoxKeys()
    // It holds the private key. Written whole beside it, then linked in only while the path is still free: a start
    // that dies midway leaves no half-written key file, and two starts at once never write over each other's keys.
    const draft = `${path}.${crypto.randomUUID()}.new`
    await writeFile(draft, await saveBoxKeys(keys), { mode: 0o600, flag: 'wx' })
    try {
      await link(draft, path)
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
      return recipientKeys(path) // another start made them first: use those
    } finally {
      await rm(draft, { force: true })
    }
    return keys
  }
  const keys = await loadBoxKeys(text.trim()) // rejects when WebCrypto fails, rather than saying there are none
  if (!keys) throw new Error(`${path} holds no box keys: move it aside to make new ones`)
  return keys
}

const keys = await recipientKeys(path)
register(keys.publicKey) // 43 characters of base64url: give it to the server, which hands it to the page

In the page, sealing to the public key the server gave it:

import { sealBox } from '@codefusion-cc/sealed/box'

const sealed = await sealBox(relay.publicKey, prompt, `terminal:${relay.id}:${promptId}`) // null: not a box public key

On the recipient, opening what the server relayed:

import { openBox } from '@codefusion-cc/sealed/box'

const opened = await openBox(keys, sealed, `terminal:${relayId}:${promptId}`)
if (opened.ok) run(opened.text) // or opened.bytes
// 'malformed': not a sealed box, or longer than maxBytes (1 MiB unless given);
// 'tampered': changed, sealed to another key, or for another context.
  • What it does: each seal makes a fresh X25519 key pair; the secret it agrees with the recipient's key goes through HKDF-SHA256 (salted with both public keys, the context in its info) to an AES-256-GCM key used once. The sealed text is box1.<ephemeral public key>.<iv>.<ciphertext>, each part base64url without padding.
  • Context: the string names where the box may be opened, as seal's does above. A box moved to another context (another prompt, another relay) does not open there. Put in it every id the recipient checks.
  • Whose key: whoever hands the page the recipient's public key decides who opens the box. A server that hands out a key of its own instead reads what the page seals, so the box keeps a secret from a server that stores and relays it (its database, its logs, a leak of either), not from one that lies about the key. Where that matters, have the page get or check the key another way (a fingerprint shown on the recipient).
  • Anonymous sender: a box says nothing of who sealed it. Whoever has the public key can seal to it, so what a box holds is input from outside: authenticate who asked for it separately (the server does, before relaying it).
  • No replay protection of its own: the same box opens again for the same context. Make the context unique (a prompt id) and have the recipient refuse an id it has already run.
  • Keys at rest: saveBoxKeys writes the private key's JWK (RFC 8037); keep it where only its owner reads it. loadBoxKeys returns null for anything else, including a JWK whose two halves are not one pair, and loads the private key non-extractable unless { extractable: true } (needed only to save it again).
  • Errors: sealBox resolves null for a recipient that is not a box public key (or one of the few X25519 keys of small order, which agree no secret); openBox never throws for what the sealed text holds. Both throw for arguments a programming mistake gives (a context that is not a string, keys that are not box keys), and each function that uses WebCrypto rejects with the platform's own error where it fails (no X25519 in older browsers, a broken binding): null, malformed and tampered only ever describe the input.
  • Small secrets: a box is one AES-GCM message, held in memory whole. openBox refuses more than maxBytes of plaintext (default 1 MiB) before decoding anything.

@codefusion-cc/realtime's account keys (packages/realtime/src/accountKeys.ts) have a box of their own, on P-256 with a format of their own, and devices already keep data sealed with it, so they stay on it rather than move here.