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/digest

v0.5.3

Published

Cryptographic content hashing and canonicalization for Effect

Readme

@scenesystems/digest

@scenesystems/digest computes content identifiers, hashes, message authentication codes, and derived keys for programs built with Effect. Use it when two processes must agree on the identity of a JSON value, when a cache or store is keyed by content, when a webhook body must be authenticated, or when key material must be derived from a shared secret.

The content digest model is a fixed pipeline: an admitted plain-data value is canonicalized with RFC 8785 JSON Canonicalization Scheme, encoded as strict UTF-8, hashed with BLAKE3-256 or SHA-256, and rendered as <algorithm>:<base64url>. A digest therefore identifies exactly one byte sequence, and any two producers that follow the same pipeline agree on it. The primitives come from Noble Hashes, which are audited, dependency-free, and run in every JavaScript runtime.

@scenesystems/effect-search and @scenesystems/effect-math use this package for cache keys and artifact identities. @scenesystems/seal and @scenesystems/sign provide encryption and signatures, which hashing alone does not.

Installation

npm install @scenesystems/digest effect

Effect ^3.22.1 is a required peer dependency. The package has one entrypoint, @scenesystems/digest.

Basic use

digest canonicalizes a plain value and returns its tagged digest. digestSchemaValue encodes a value through an Effect Schema first, so the digest reflects the wire representation rather than the in-memory one.

import { digest, digestSchemaValue } from "@scenesystems/digest"
import { Effect, Schema } from "effect"

const Event = Schema.Struct({
  name: Schema.String,
  occurredAt: Schema.DateFromString
})

export const program = Effect.gen(function* () {
  const contentId = yield* digest("blake3-256", { score: 42, user: "alice" })
  const eventId = yield* digestSchemaValue(Event, {
    name: "deploy",
    occurredAt: new Date("2026-07-22T00:00:00.000Z")
  })
  return { contentId, eventId }
})

Both return <algorithm>:<base64url>; a 256-bit digest has 43 unpadded base64url characters after the tag. Key order in the input does not matter, because canonicalization sorts keys. Failures surface in the error channel as CanonicalizationError and, for schema values, ParseResult.ParseError.

Content digests

digest(algorithm, value) is the general entry point, with blake3-256 and sha256 as the supported algorithms. durableFingerprint(value) is the same pipeline fixed to BLAKE3-256, intended for identifiers that are stored and compared across releases. canonicalize(value) returns the canonical JSON string and canonicalJsonBytes(value) its UTF-8 bytes, for callers that need the preimage itself.

digestSchemaValue(schema, value, algorithm?) applies Schema.encode and keeps any service requirements the schema declares. When the value comes from an untrusted source and could be large, use digestSchemaValueWithByteLimit(schema, value, maximumBytes, algorithm?). It stops emitting canonical bytes at the inclusive limit, fails with CanonicalByteLimitExceeded, and on success returns the digest with the exact canonicalByteLength. digestSchemaValueWithByteLimitSync has the same contract and returns an Either for small, owner-controlled values in code that cannot run an Effect.

import { digestSchemaValueWithByteLimit } from "@scenesystems/digest"
import { Effect, Schema } from "effect"

const Payload = Schema.Struct({ id: Schema.String, tags: Schema.Array(Schema.String) })

export const identify = (payload: typeof Payload.Type) =>
  digestSchemaValueWithByteLimit(Payload, payload, 64 * 1024).pipe(
    Effect.map((result) => ({ id: result.digest, bytes: result.canonicalByteLength })),
    Effect.catchTag("CanonicalByteLimitExceeded", () => Effect.succeed({ id: "too-large", bytes: -1 }))
  )

The byte limit bounds output, not traversal. Structurally unbounded input, such as deeply nested or extremely wide objects, must be limited by the caller before it reaches canonicalization.

Canonicalization contract

canonicalize accepts null, booleans, finite numbers, well-formed Unicode strings, dense arrays of accepted values, and plain records whose prototype is Object.prototype or null. Record properties must be own, enumerable, string-keyed data properties. Everything else is rejected with UnsupportedValue: undefined, NaN and infinities, bigint, functions, symbols, sparse arrays, typed arrays, dates, regular expressions, maps, sets, promises, accessors, and class instances. Cycles are reported as CyclicValue. Getters are never evaluated.

Strings must be well-formed UTF-16. A lone surrogate is reported as InvalidUnicode with its index; valid text is preserved byte for byte without Unicode normalization. Keys sort by UTF-16 code unit as RFC 8785 requires. The input must not be mutated while canonicalization runs.

The contract is deliberately narrow so that a digest computed in one runtime, language, or release matches a digest computed in another. Convert domain objects to plain data with a Schema before digesting them; that is what digestSchemaValue does for you.

Bytes, streams, and authentication

For data that is already bytes or text, skip canonicalization. digestBytes(algorithm, bytes) and digestUtf8(algorithm, text) return raw digest bytes; the Base64Url and Hex suffixed variants return encoded strings. blake3Hash and sha256 are the underlying one-shot functions.

digestByteStream(algorithm, stream) and digestUtf8Stream(algorithm, stream) hash an Effect Stream incrementally without buffering it, preserving the stream's error and requirement types. The text variant handles surrogate pairs split across chunks and reports malformed text at its absolute index.

Hashing does not authenticate. To prove that a message came from a holder of a shared key, use hmacSha256(key, message), hmacSha1 where a protocol requires it, or blake3Mac(key, message), which needs exactly a 32-byte key. hkdfSha256(ikm, salt, info, length), hkdfSha512, and blake3DeriveKey(context, input, length) derive keys from shared material.

import { encodeUtf8, hmacSha256, hmacSha256Base64Url, toBase64Url } from "@scenesystems/digest"
import { Effect } from "effect"

export const signWebhook = (secret: string, body: string) =>
  Effect.gen(function* () {
    const key = yield* encodeUtf8(secret)
    const payload = yield* encodeUtf8(body)
    return yield* hmacSha256Base64Url(key, payload)
  })

export const authenticatorBytes = (key: Uint8Array, payload: Uint8Array) =>
  Effect.map(hmacSha256(key, payload), (mac) => ({ mac, encoded: toBase64Url(mac) }))

Compare a received authenticator with a recomputed one using a constant-time comparison, and bind the algorithm, key identity, and message domain in the surrounding protocol. Secret text must be encoded to bytes with encodeUtf8 or decoded from its wire encoding with fromBase64Url or fromHex before it is used as a key.

Public surface

The package exports plain functions and schemas from a single entrypoint.

| Area | Exports | | --------------- | ------------------------------------------------------------------------------------------- | | Content digests | digest, durableFingerprint, canonicalize, canonicalJsonBytes | | Schema values | digestSchemaValue, digestSchemaValueWithByteLimit, digestSchemaValueWithByteLimitSync | | Bytes and text | digestBytes, digestUtf8, their Base64Url and Hex variants, blake3Hash, sha256 | | Streams | digestByteStream, digestUtf8Stream, their Base64Url and Hex variants | | Authentication | hmacSha256, hmacSha1, blake3Mac, and their encoded variants | | Key derivation | hkdfSha256, hkdfSha512, blake3DeriveKey | | Encoding | encodeUtf8, toBase64Url, fromBase64Url, toHex, fromHex | | Schemas | DigestAlgorithm, Digest256, ContentDigest, and the error classes |

The full list with signatures is in the API reference.

Errors and boundaries

CanonicalizationError is the closed union InvalidUnicode | UnsupportedValue | CyclicValue. CanonicalByteLimitError is CanonicalByteLimitExceeded | InvalidCanonicalByteLimit. blake3Mac fails with InvalidKeyLength for any key that is not 32 bytes. The decoders fromBase64Url and fromHex return Either with Effect's decode error. All error values carry bounded diagnostics and never include rejected text, keys, paths, canonical preimages, or digest material.

The package establishes content identity and nothing more. Authenticity requires a MAC or signature, confidentiality requires encryption, and key storage, rotation, versioning, and comparison rules belong to the protocol around it.

Standards

The test suite checks against published vectors for RFC 8785 JCS, the BLAKE3 specification, FIPS 180-4 SHA-256, RFC 2104 HMAC, and RFC 5869 HKDF, with additional HMAC and HKDF vectors from RFC 4231, RFC 2202, and Project Wycheproof. Noble's audits cover the primitive implementations; the admission rules, option selection, and error mapping in this package are reviewed separately.

Examples

The examples directory contains one runnable program per capability: content hashing, webhook verification with HMAC, content addressing, and streaming digests.

Status

This package is pre-1.0. Minor releases may change public APIs; pin a compatible version and review the changelog when upgrading.

Contributing and support

Read the repository contributing guide before opening a pull request. Report defects and request changes through GitHub issues. For security concerns, follow the security policy.

License

MIT. Copyright 2026 Scene Systems.