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

@scenesystems/seal

v0.3.0

Published

Authenticated encryption for Effect

Readme

@scenesystems/seal

Authenticated encryption for Effect v4, backed by Noble Ciphers. Effect ^4.0.0 is a required peer dependency.

Cipher owns the injectable backend and the byte-valued Cipher.Encrypted model. Envelope owns its base64url transport codec. Identity, authorization, key storage, rotation, and algorithm policy belong to the application.

Encrypt bytes, encode at the boundary

import { Cipher, Envelope } from "@scenesystems/seal"
import { Effect, Schema } from "effect"

export const program = Effect.gen(function* () {
  const key = yield* Cipher.generateKey
  const plaintext = yield* Schema.decodeEffect(Schema.Uint8ArrayFromHex)("0001027f80ff")
  const encrypted = yield* Cipher.encrypt("xchacha20-poly1305", key, plaintext)
  const codec = Schema.fromJsonString(Envelope.Envelope)
  const json = yield* Schema.encodeEffect(codec)(encrypted)
  const decoded = yield* Schema.decodeEffect(codec)(json)
  return yield* Cipher.decrypt(decoded, key)
}).pipe(Effect.provide(Cipher.layer))

Cipher.Encrypted has algorithm, nonce, and ciphertext fields. The byte-valued ciphertext includes its authentication tag. There is no packed nonce-prefixed representation or pack/unpack API. Construction validates shape, not lengths or authenticity, and borrows supplied byte buffers rather than copying them. Effect's structural equality is not a constant-time secret comparison.

Cipher.encrypt returns fresh nonce and ciphertext buffers; Cipher.decrypt returns fresh plaintext. Neither mutates its inputs. Keep caller-owned bytes unchanged until an operation completes. Decryption checks exact nonce length and minimum tag length before authenticating, and does not acquire entropy.

Transport uses Effect's byte codecs

Envelope.Envelope is a codec, not a second model class. Its decoded value is Cipher.Encrypted; its encoded value (Envelope.Encoded) contains the same field names with base64url strings. Encoding emits unpadded base64url. Decoding validates encoding and allocates fresh byte fields, but does not authenticate or enforce algorithm-specific lengths.

import { Cipher, Envelope } from "@scenesystems/seal"
import { Effect, Schema } from "effect"

export const openStored = (key: Uint8Array, stored: unknown) =>
  Schema.decodeUnknownEffect(Envelope.Envelope)(stored).pipe(
    Effect.flatMap((encrypted) => Cipher.decrypt(encrypted, key))
  )

export const sealForTransport = (key: Uint8Array, plaintext: Uint8Array) =>
  Envelope.encrypt("xchacha20-poly1305", key, plaintext)

export const openTyped = (key: Uint8Array, stored: Envelope.Encoded) => Envelope.decrypt(stored, key)

Envelope.encrypt combines encryption and encoding; Envelope.decrypt combines decoding and authentication, also supporting pipeable Envelope.decrypt(key). Both preserve the Cipher.Cipher requirement. Unknown input should go through schema admission, whose schema errors are distinct from Cipher's sanitized failures; do not expose input-bearing schema diagnostics to an untrusted peer.

The algorithm field is metadata, not authenticated AAD. Enforce the protocol's chosen algorithm before decryption. There is no AAD or caller-supplied nonce API.

Algorithms and key lifecycle

| Algorithm | Nonce | Contract | | -------------------- | -------: | --------------------------------------------------------- | | xchacha20-poly1305 | 24 bytes | Recommended for randomly generated nonces. | | aes-256-gcm-siv | 12 bytes | Resists accidental nonce reuse; usage limits still apply. | | aes-256-gcm | 12 bytes | Nonce reuse under one key is catastrophic. |

All algorithms use 32-byte keys and 16-byte tags. All-zero keys are rejected as Theoria policy, not an AEAD standard requirement. Cipher.generateKey produces a fresh key on every execution. Cipher.layer uses the host CSPRNG through Noble, never Effect's seedable Random service; provide it at the host boundary. Layer construction does not acquire entropy.

For 96-bit random nonces, keep message counts per AES key well below 2^32. Noble recommends around 2^23 messages for a collision probability near 2^-50. Choose stricter limits when required and rotate keys on an application-owned schedule. Do not reuse keys across protocols without a domain and lifecycle analysis.

Typed failures

  • Cipher.InvalidKey: wrong length or all-zero key; carries only expected/received lengths and a fixed reason.
  • Cipher.DecryptionFailed: malformed transport encoding uses invalid envelope encoding; wrong keys, wrong field lengths, and modified ciphertext use authentication failed.
  • Cipher.EncryptionFailed: encryption or secure nonce acquisition failed; carries only the algorithm.
  • Cipher.KeyGenerationFailed: secure key generation failed.

These use Data.TaggedError and retain no key/plaintext material or primitive causes. Use Effect.catchTag; protocols own outward error representations. Treat authentication failures uniformly.

Migration and verification

This breaking pre-1.0 release requires Effect v4. Import Cipher and Envelope from the package root or matching public subpaths. Flat exports and compatibility aliases are not retained. Use Cipher.encrypt / Cipher.decrypt with Cipher.Encrypted, and Effect schema encoding/decoding with Envelope.Envelope. Envelope.fromBytes and Envelope.toBytes are removed. Count cryptographic payload bytes as nonce length plus ciphertext length, not JSON length.

Independent Wycheproof and RFC 8452 known answers cover all algorithms. Tests cover seeded byte-preservation properties, JSON codecs, independent buffer ownership, invalid fields, key boundaries, entropy failure, lazy acquisition, and interruption/finalization. Noble's audits cover the primitives, not application protocol policy.

See examples, tests, contributing, and security.

MIT. Copyright 2026 Scene Systems.