- Title
- Description
Globally Qualified Electronic Attestation of Attributes
what it's for
what it’s not for
digital signatures that do not need recognigtion from a governance
- 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) returnimport { 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