- Title
- Description
1–2 sentences:
what it is
what it’s for
(optional) what it’s not for
- Compatibility
A small bullet list, always in this order:
Runtimes: Node >= X; Browsers: ; Workers/Edge:
Module format: ESM/CJS
Required globals / APIs: crypto.subtle, CompressionStream, indexedDB, etc.
TypeScript: bundled types / source TS / etc.
- Goals
3–6 bullets, phrased as outcomes:
“Developer-friendly API…”
“No deps…”
“Returns copies for safety…”
- Installation
Always the same triple:
npm install
pnpm add
yarn add
- Usage
Smallest runnable example (the “copy/paste test”)
Common patterns: 2–5 short subsections max (don’t turn this into an API book)
- Runtime behavior
This is where you put the “how it behaves in different environments” and “what errors look like”.
Use a consistent set of subheadings when relevant:
Node
Browsers / Edge runtimes
Validation & errors
Safety / copying semantics
Caching semantics (if applicable)
- Tests
This section should answer:
What test types exist? (unit / integration / e2e / type tests)
Where do they run? (Node versions; browser matrix)
Coverage: tool + percentage (and what it measures)
Status claim: “passes on …” (keep it factual)
Example format:
Suite: unit (Node), integration, E2E (Playwright)
Matrix: Chromium / Firefox / WebKit (+ mobile emulation if you do it)
Coverage: c8 — 100% statements/branches/functions/lines (Node)
Notes: any known skips
- Benchmarks
This should show actual numbers, plus reproduction context.
Minimum content:
How it was run: command
Environment: runtime version + platform
Results: table or block of key ops/s or timings
Disclaimer: results vary by machine
- License
One line. Always last.
EXAMPLES:
Typed JavaScript byte utilities for base64url, UTF-8 strings, JSON, and gzip that behave the same in browsers and Node. Built to make JavaScript/TypeScript projects with lots of byte-format data a breeze to build, without having to write your own utilities or boilerplate.
- Runtimes: Node >= 18; Browsers: modern browsers with TextEncoder/TextDecoder + btoa/atob; Workers/Edge: runtimes with TextEncoder/TextDecoder + btoa/atob (gzip needs CompressionStream/DecompressionStream).
- Module format: ESM-only (no CJS build).
- Required globals / APIs: Node
Buffer(base64/UTF-8 fallback); browser/edgeTextEncoder,TextDecoder,btoa,atob; gzip in browser/edge needsCompressionStream/DecompressionStream. - TypeScript: bundled types.
- Developer-friendly API for base64url, UTF-8, JSON, gzip, concat, and equality.
- No dependencies or bundler shims.
- ESM-only and side-effect free for tree-shaking.
- Returns copies for safety when normalizing inputs.
- Consistent behavior across Node, browsers, and edge runtimes.
npm install @z-base/bytecodec
# or
pnpm add @z-base/bytecodec
# or
yarn add @z-base/bytecodecimport { Bytes } from "@z-base/bytecodec";
// The `Bytes` convenience class wraps the same functions as static methods.
const encoded = Bytes.toBase64UrlString(new Uint8Array([1, 2, 3]));import { toBase64UrlString, fromBase64UrlString } from "@z-base/bytecodec";
const bytes = new Uint8Array([104, 101, 108, 108, 111]);
const encoded = toBase64UrlString(bytes); // string of base64url chars
const decoded = fromBase64UrlString(encoded); // Uint8Arrayimport { fromString, toString } from "@z-base/bytecodec";
const textBytes = fromString("caffe and rockets"); // Uint8Array
const text = toString(textBytes); // "caffe and rockets"import { fromJSON, toJSON } from "@z-base/bytecodec";
const jsonBytes = fromJSON({ ok: true, count: 3 }); // Uint8Array
const obj = toJSON(jsonBytes); // { ok: true, count: 3 }import { toCompressed, fromCompressed } from "@z-base/bytecodec";
const compressed = await toCompressed(new Uint8Array([1, 2, 3])); // Uint8Array
const restored = await fromCompressed(compressed); // Uint8Arrayimport { toUint8Array, toArrayBuffer, toBufferSource } from "@z-base/bytecodec";
const normalized = toUint8Array([1, 2, 3]); // Uint8Array
const copied = toArrayBuffer(normalized); // ArrayBuffer
const bufferSource = toBufferSource(normalized); // Uint8Array as BufferSourceimport { equals } from "@z-base/bytecodec";
const isSame = equals(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3])); // true | falseimport { concat } from "@z-base/bytecodec";
const joined = concat([new Uint8Array([1, 2]), new Uint8Array([3, 4]), [5, 6]]); // Uint8ArrayUses Buffer.from for base64 and TextEncoder/TextDecoder when available, with Buffer fallback; gzip uses node:zlib.
Uses TextEncoder/TextDecoder and btoa/atob. Gzip uses CompressionStream/DecompressionStream when available.
Validation failures throw BytecodecError with a code string (for example BASE64URL_INVALID_LENGTH, UTF8_DECODER_UNAVAILABLE, GZIP_COMPRESSION_UNAVAILABLE), while underlying runtime errors may bubble through.
Normalization helpers return copies (Uint8Array/ArrayBuffer) to avoid mutating caller-owned buffers.
Suite: unit + integration (Node), E2E (Playwright) Matrix: Chromium / Firefox / WebKit + mobile emulation (Pixel 5, iPhone 12) Coverage: c8 — 100% statements/branches/functions/lines (Node) Notes: no known skips
How it was run: node benchmark/bench.js
Environment: Node v22.14.0 (win32 x64)
Results:
| Benchmark | Result |
|---|---|
| base64 encode | 514,743 ops/s (97.1 ms) |
| base64 decode | 648,276 ops/s (77.1 ms) |
| utf8 encode | 1,036,895 ops/s (48.2 ms) |
| utf8 decode | 2,893,954 ops/s (17.3 ms) |
| json encode | 698,985 ops/s (28.6 ms) |
| json decode | 791,690 ops/s (25.3 ms) |
| concat 3 buffers | 617,497 ops/s (81.0 ms) |
| toUint8Array | 10,149,502 ops/s (19.7 ms) |
| toArrayBuffer | 620,992 ops/s (322.1 ms) |
| toBufferSource | 8,297,585 ops/s (24.1 ms) |
| equals same | 4,035,195 ops/s (49.6 ms) |
| equals diff | 2,760,784 ops/s (72.4 ms) |
| gzip compress | 10,275 ops/s (38.9 ms) |
| gzip decompress | 18,615 ops/s (21.5 ms) |
Results vary by machine.
Apache
Developer-experience-first cryptography toolkit that lets you powerfully express cryptographic intentions through a semantic and declarative API surface.
- Runtimes: Modern JavaScript hosts with WebCrypto.
- Module format: ESM-only (no CJS build).
- Required globals / APIs:
crypto,crypto.subtle,crypto.getRandomValues. - TypeScript: bundled types.
- Consistent JWK validation for AES-GCM, HMAC, Ed25519, and RSA-OAEP.
- Byte-oriented APIs (
Uint8ArrayandArrayBuffer) to avoid ambiguous inputs. - No side effects on import; all work happens per call.
- Clean separation between agents (stateful) and clusters (cached).
- Minimal, but strict WebCrypto wrappers with explicit
CryptosuiteErrorcodes.
npm install @z-base/cryptosuite
# or
pnpm add @z-base/cryptosuite
# or
yarn add @z-base/cryptosuiteimport { Cryptosuite } from "@z-base/cryptosuite";
// The `Cryptosuite` convenience class wraps classes and functions into an intuitive structure.
const cipherJwk = await Cryptosuite.cipher.generateKey();
const payload = new Uint8Array([1, 2, 3]);
const artifact = await Cryptosuite.cipher.encrypt(cipherJwk, payload);
const roundtrip = await Cryptosuite.cipher.decrypt(cipherJwk, artifact);import {
deriveOID,
generateOID,
validateOID,
type OpaqueIdentifier,
} from "@z-base/cryptosuite";
const oid = await generateOID(); // 43 random base64url chars
const derived = await deriveOID(idBytesFromSomewhere); // 43 deterministic base64url chars
const valid = validateOID(uncontrolledOID); // 43 base64url chars | false
if (!valid) return;import { fromJSON, toJSON } from "@z-base/bytecodec";
import {
deriveCipherKey,
CipherCluster,
CipherAgent,
type CipherJWK,
} from "@z-base/cryptosuite";
const cipherJwk = await deriveCipherKey(deterministicBytes);
const state = { name: "Bob", email: "bob@email.com" };
const enc = await CipherCluster.encrypt(cipherJwk, fromJSON(state)); // {iv, ciphertext}
const dec = await CipherCluster.decrypt(cipherJwk, enc);
const restored = toJSON(dec);
console.log(restored.name); // "Bob"import { fromString, toString } from "@z-base/bytecodec";
import {
generateCipherKey,
generateExchangePair,
ExchangeCluster,
WrapAgent,
type WrapJWK,
UnwrapAgent,
type UnwrapJWK,
CipherAgent,
type CipherJWK,
} from "@z-base/cryptosuite";
const { wrapJwk, unwrapJwk } = await generateExchangePair();
const encryptJwk = await generateCipherKey();
const encryptAgent = new CipherAgent(encryptJwk);
const body = await encryptAgent.encrypt(fromString("Hello world!")); // {iv, ciphertext}
const header = await ExchangeCluster.wrap(wrapJwk, encryptJwk); // ArrayBuffer
const message = { header, body };
const decryptJwk = (await ExchangeCluster.unwrap(
unwrapJwk,
message.header,
)) as CipherJWK;
const decryptAgent = new CipherAgent(decryptJwk);
const decryptedBody = await decryptAgent.decrypt(message.body);
const messageText = toString(decryptedBody); // "Hello world!"import { fromString } from "@z-base/bytecodec";
import {
generateHMACKey,
HMACCluster,
HMACAgent,
type HMACJWK,
} from "@z-base/cryptosuite";
const hmacJwk = await generateHMACKey();
const challenge = crypto.getRandomValues(new Uint8Array(32));
const sig = await HMACCluster.sign(hmacJwk, challenge); // ArrayBuffer
const ok = await HMACCluster.verify(hmacJwk, challenge, sig); // true | falseimport {
generateVerificationPair,
VerificationCluster,
SignAgent,
type SignJWK,
VerifyAgent,
type VerifyJWK,
} from "@z-base/cryptosuite";
const { signJwk, verifyJwk } = await generateVerificationPair();
const payload = new Uint8Array([9, 8, 7]);
const sig = await VerificationCluster.sign(signJwk, payload); // ArrayBuffer
const ok = await VerificationCluster.verify(verifyJwk, payload, sig); // true | falseUses Node's global WebCrypto (globalThis.crypto) when available. Node is not the primary target, but tests and benchmarks run on Node 18+.
Uses crypto.subtle and crypto.getRandomValues. Ed25519 and RSA-OAEP support vary by engine; unsupported operations throw CryptosuiteError codes.
Validation failures throw CryptosuiteError with a code string (for example AES_GCM_KEY_EXPECTED, RSA_OAEP_UNSUPPORTED, ED25519_ALG_INVALID). Cryptographic failures (e.g., decrypt with the wrong key) bubble the underlying WebCrypto error.
- Keep
{iv, ciphertext}together and never mix IVs across messages or keys. - Treat all JWKs and raw key bytes as secrets; never log them and rotate on exposure.
- Always sign a canonical byte serialization so verifiers see identical bytes.
- Ciphertext length leaks; add padding at your protocol layer if size is sensitive.
- Handle decrypt/verify failures uniformly; don't leak which check failed.
Suite: unit + integration (Node), E2E (Playwright) Matrix: Chromium / Firefox / WebKit + mobile emulation (Pixel 5, iPhone 12) Coverage: c8 — 100% statements/branches/functions/lines (Node)
How it was run: node benchmark/bench.js
Environment: Node v22.14.0 (win32 x64)
Results:
| Benchmark | Result |
|---|---|
| AES-GCM encrypt | 30.41ms (6575.9 ops/sec) |
| HMAC sign+verify | 29.95ms (6678.1 ops/sec) |
| Ed25519 sign+verify | 76.45ms (2616.0 ops/sec) |
| RSA-OAEP wrap+unwrap | 1224.07ms (163.4 ops/sec) |
Results vary by machine.
Apache
Client-side WebAuthn credential discovery for strict zero-knowledge apps. Deterministically derive a routing identifier and cryptographic root keys from a user-verifying authenticator, without accounts, identifiers, or server-side state.
- Runtimes: modern browsers with WebAuthn + PRF extension + user verification.
- Module format: ESM-only (no CJS build).
- Required globals / APIs:
window,navigator.credentials,PublicKeyCredential, PRF extension,crypto.subtle,crypto.getRandomValues. - TypeScript: bundled types.
- Enable strict local-first zero-knowledge for browsers.
- Deterministic, runtime-only derivation of an opaque ID and root keys.
- No storage, no networking, no server-side requirements.
- Explicit failure modes with stable error codes.
npm install @z-base/zero-knowledge-credentials
# or
pnpm add @z-base/zero-knowledge-credentials
# or
yarn add @z-base/zero-knowledge-credentialsThese give a general idea and MUST NOT be interpreted as a full solution.
import {
ZKCredentials,
type ZKCredential,
type ZKCredentialErrorCode,
} from "@z-base/zero-knowledge-credentials";
await ZKCredentials.registerCredential(
"User display name",
"platform", // or 'cross-platform'
);import { Bytes } from "@z-base/bytecodec";
import { Cryptosuite } from "@z-base/cryptosuite";
import { ZKCredentials } from "@z-base/zero-knowledge-credentials";
const root = await ZKCredentials.discoverCredential();
const id = root.id; // routing identifier / OpaqueIdentifier
const hmacJwk = root.hmacJwk; // HMAC root key / HMACJWK
const cipherJwk = root.cipherJwk; // AES-GCM root key / CipherJWK
const cache = await caches.open("opaque-blobs");
let artifact = await cache.match(id); // {iv, ciphertext}
if (!artifact) {
const challengeRaw = await fetch(`/api/v1/artifact/${id}/challenge`);
const challengeText = await challengeRaw.text();
const challengeBytes = Bytes.fromBase64UrlString(challengeText);
const signature = await Cryptosuite.hmac.sign(hmacJwk, challengeBytes);
const raw = await fetch(`/api/v1/artifact/${id}`, {
headers: {
Authorization: Bytes.toBase64UrlString(signature),
},
});
artifact = await raw.json(); // {iv, ciphertext}
}
const accountCredentials = await Cryptosuite.cipher.decrypt(
cipherJwk,
artifact,
);
// const {id, hmacJwk, cipherJwk} = accountCredentials
// repeat...
// const {profileCredentials, workspaceCredentials} = resourceCredentialsimport { Bytes } from "@z-base/bytecodec";
import { Cryptosuite } from "@z-base/cryptosuite";
import { ZKCredentials } from "@z-base/zero-knowledge-credentials";
const profile = {
name: "Bob",
preferences: {
theme: "dark",
},
};
const credentials = await ZKCredentials.generateCredential();
const id = credentials.id; // resource routing identifier / OpaqueIdentifier
const hmacJwk = credentials.hmacJwk; // HMAC resource key / HMACJWK
const cipherJwk = credentials.cipherJwk; // AES-GCM resource key / CipherJWK
const profileBytes = Bytes.fromJSON(profile);
const artifact = await Cryptosuite.cipher.encrypt(cipherJwk, profileBytes);
fetch(
`/api/v1/artifact/${id}`,
JSON.stringify({
verifier: hmacJwk,
state: {
iv: Bytes.toBase64UrlString(artifact.iv),
ciphertext: Bytes.toBase64UrlString(artifact.ciphertext),
},
}),
{
method: "POST",
},
);Uses WebAuthn PRF outputs to derive:
id(SHA-256 -> base64url ofrawId)cipherJwk(AES-GCM)hmacJwk(HMAC-SHA256)
All failures are explicit and semantic. Errors are instances of ZKCredentialError with a stable code:
unsupportedaborteduser-deniedno-credentialprf-unavailablekey-derivation-failed
Suite: unit + integration (Node), E2E (Playwright) Matrix: Chromium / Firefox / WebKit + mobile emulation (Pixel 5, iPhone 12) Coverage: c8 — 100% statements/branches/functions/lines (dist via source maps)
How it was run: npm run bench
Environment: Node v22.14.0 (win32 x64)
Results:
| Benchmark | Result |
|---|---|
| fromPRF | 5,224 ops/s (0.191 ms/op, 200 ops) |
| generateCredential | 5,825 ops/s (0.172 ms/op, 50 ops) |
Results vary by machine.
Apache