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

@otskit/client

v0.5.0

Published

Official client SDK for OpenTimestamps calendars with resilience patterns

Readme

@otskit/client

TypeScript/JavaScript client for OpenTimestamps with enterprise-grade resilience patterns

CI CodeQL npm version npm downloads TypeScript Node ≥20 License: MIT

@otskit/client is the official client SDK for submitting, upgrading, and verifying OpenTimestamps proofs. It sits on top of @otskit/core — the low-level protocol engine — and wraps it in a high-level API with production-ready resilience patterns built in.

Features

Complete OpenTimestamps Workflow

  • stamp() — Hash your data, build a Merkle tree with a secure nonce, and submit to multiple calendar servers simultaneously
  • upgrade() — Query calendars for Bitcoin confirmations and merge them into the pending proof
  • verify() — Verify a completed proof against the Bitcoin blockchain via Esplora

Enterprise-Grade Resilience

  • Circuit Breaker — Per-calendar isolation; one failing calendar never affects the others
  • Exponential Backoff — Three strategies (exponential, linear, constant) with three jitter modes (full, equal, none)
  • Dual Timeouts — Independent totalTimeoutMs (whole operation) and connectTimeoutMs (per attempt)
  • Threshold Submissionsstamp() requires N-of-M successful submissions (default 2-of-4); configurable
  • Fail-Fast on 4xx — Client errors are never retried; only 5xx and network failures trigger retries

Developer Experience

  • TypeScript-first — Strict types throughout; full IntelliSense for every option and error
  • Node.js 20+ — Requires Node.js; uses native crypto, dns, and net APIs not available in browsers or edge runtimes
  • Tree-shakeable — Dual ESM/CJS build, zero runtime dependencies
  • AbortController support — Cancel any in-flight operation at any level
  • Observable — Drop-in Logger interface compatible with console, pino, winston, etc.
  • Built-in SHA-256 helpershashFile() and hashBuffer() so you don't need to wire up crypto yourself

Note on confirmation times: After stamp(), the proof is pending — registered with calendar servers but not yet anchored to Bitcoin. Confirmations typically arrive within 10–60 minutes, but can take several hours during periods of high network congestion. Call upgrade() periodically to check; an UpgradeError simply means the blockchain hasn't confirmed yet, not that anything went wrong.


Installation

npm install @otskit/client

@otskit/core is a peer dependency bundled as a file: reference in monorepo setups; no separate install is needed.


Quick Start

import { OpenTimestampsClient, hashFile } from '@otskit/client'
import { writeFileSync } from 'fs'

const client = new OpenTimestampsClient()

// 1. Hash the file you want to timestamp
const hash = await hashFile('contract.pdf')

// 2. Submit to calendars → get a pending .ots proof
const pendingProof = await client.stamp(hash)
writeFileSync('contract.pdf.ots', pendingProof)
console.log('Proof saved — Bitcoin confirmation usually arrives in 10–60 minutes.')

// 3. Later: query calendars for a Bitcoin confirmation
const upgradedProof = await client.upgrade(pendingProof)
writeFileSync('contract.pdf.ots', upgradedProof)

// 4. Verify the completed proof
const result = await client.verify(upgradedProof, hash)
if (result.valid) {
  console.log(`Timestamp confirmed in Bitcoin block ${result.blockHeight}`)
  console.log(`Block time: ${new Date(result.timestamp! * 1000).toISOString()}`)
} else {
  console.error(`Verification failed: ${result.error}`)
}

Usage

Hashing files and data

Use the built-in helpers to compute SHA-256 without importing crypto yourself:

import { hashFile, hashBuffer } from '@otskit/client'

// From a file path (streaming — safe for large files)
const hash = await hashFile('contract.pdf')

// From bytes already in memory
const hash = hashBuffer(Buffer.from('hello world'))
const hash = hashBuffer(new Uint8Array([...]))

Both return a 32-byte Buffer ready to pass directly to stamp().

Stamping data

stamp() accepts either a 32-byte Buffer or a 64-character hex string:

// From hashFile / hashBuffer
const proof = await client.stamp(await hashFile('contract.pdf'))

// Or a hex string
const proof = await client.stamp('a'.repeat(64))

Internally, stamp() prepends a 16-byte cryptographic nonce to each submission, builds a Merkle tree over all concurrent submissions, and serializes the result as a standard .ots file.

Upgrading a pending proof

Call upgrade() periodically until Bitcoin confirms the timestamp. It queries only the calendars embedded in the proof (validated against a whitelist), so your calendars option does not affect this step.

import { UpgradeError } from '@otskit/client'

try {
  const upgradedProof = await client.upgrade(pendingProof)
  // Save and stop polling
} catch (err) {
  if (err instanceof UpgradeError) {
    // No calendar has a Bitcoin confirmation yet — try again later
    console.log('Not confirmed yet, retry in 5 minutes')
  }
}

Verifying a proof

verify() queries the Blockstream Esplora API to check the Bitcoin merkle root. Passing originalDataHash adds an extra integrity check that the proof was created for that specific hash.

const result = await client.verify(proof, originalHash)

if (result.valid) {
  console.log(result.blockHeight)  // Bitcoin block number
  console.log(result.blockHash)    // Block hash (hex)
  console.log(result.timestamp)    // Unix timestamp of the block
} else {
  console.log(result.error)        // Human-readable reason
}

verify() always returns VerificationResult — it never throws for invalid proofs, only for unexpected network failures.

Error handling

import {
  StampError,
  UpgradeError,
  ValidationError,
  NetworkError,
  CircuitBreakerError,
} from '@otskit/client'

try {
  await client.stamp(hash)
} catch (err) {
  if (err instanceof ValidationError) {
    // Invalid hash format
  } else if (err instanceof StampError) {
    // Not enough calendars accepted the submission
    console.log(`Succeeded: ${err.successfulSubmissions.map(s => s.calendar)}`)
    console.log(`Failed:    ${err.failedSubmissions.map(s => s.calendar)}`)
  } else if (err instanceof CircuitBreakerError) {
    // A calendar is isolated due to repeated failures
  } else if (err instanceof NetworkError) {
    console.log(`HTTP status: ${err.status}`) // undefined for non-HTTP errors
  }
}

Cancellation with AbortController

You can cancel individual operations or set a client-wide signal:

// Per-operation cancellation
const controller = new AbortController()
setTimeout(() => controller.abort(), 10_000)

const proof = await client.stamp(hash, { signal: controller.signal })

// Client-wide cancellation (applies to all operations)
const clientController = new AbortController()
const client = new OpenTimestampsClient({ signal: clientController.signal })

clientController.abort() // cancels any in-flight request

Observability with a logger

Any object with debug, info, warn, and error methods works:

import pino from 'pino'

const client = new OpenTimestampsClient({
  logger: pino({ level: 'debug' }),
})

Using console directly:

const client = new OpenTimestampsClient({ logger: console })

Monitoring circuit breakers

const state = client.getCircuitState('https://alice.btc.calendar.opentimestamps.org')
// 'CLOSED' | 'OPEN' | 'HALF_OPEN' | undefined

// Manually recover a calendar after a known incident
client.resetCircuit('https://alice.btc.calendar.opentimestamps.org')

// Reset all calendars at once
client.resetAllCircuits()

Configuration

ClientOptions

const client = new OpenTimestampsClient({
  // Calendar servers to submit to (default: the four public OTS calendars)
  calendars: [
    'https://alice.btc.calendar.opentimestamps.org',
    'https://bob.btc.calendar.opentimestamps.org',
    'https://finney.calendar.eternitywall.com',
    'https://btc.calendar.catallaxy.com',
  ],

  // How many calendars must succeed for stamp() to resolve (default: 2)
  minimumSuccessfulSubmissions: 2,

  // Resilience configuration (see below)
  resilience: { ... },

  // Logger implementing { debug, info, warn, error }
  logger: console,

  // AbortSignal applied to all operations on this client
  signal: controller.signal,
})

ResilienceOptions

All fields are optional — unspecified fields fall back to the defaults shown.

resilience: {
  // Maximum total time for a single operation across all retries (ms)
  totalTimeoutMs: 30_000,   // default

  // Maximum time for a single HTTP attempt (ms)
  connectTimeoutMs: 5_000,  // default

  retries: {
    enabled: true,          // default
    maxAttempts: 3,         // default

    backoff: {
      strategy: 'exponential', // 'exponential' | 'linear' | 'constant'
      initialDelayMs: 200,     // default
      maxDelayMs: 5_000,       // default; caps the computed delay
      jitter: 'full',          // 'full' | 'equal' | 'none'
    },
  },

  circuitBreaker: {
    enabled: true,            // default
    failureThreshold: 5,      // consecutive failures before OPEN (default)
    recoveryTimeoutMs: 15_000,// time in OPEN before trying HALF_OPEN (default)
    halfOpenMaxAttempts: 1,   // probing requests in HALF_OPEN state (default)
  },
}

Backoff strategies:

| Strategy | Delay formula | |---|---| | exponential | initialDelayMs × 2^(attempt - 1) | | linear | initialDelayMs × attempt | | constant | initialDelayMs |

Jitter modes:

| Mode | Effect | |---|---| | full | Random value in [0, delay] — best for thundering-herd prevention | | equal | Random value in [delay/2, delay] | | none | Deterministic delay |

Circuit breaker states:

CLOSED ──(failureThreshold consecutive failures)──► OPEN
OPEN   ──(recoveryTimeoutMs elapsed)             ──► HALF_OPEN
HALF_OPEN ──(success)                            ──► CLOSED
HALF_OPEN ──(failure)                            ──► OPEN

API Reference

OpenTimestampsClient

Constructor

new OpenTimestampsClient(options?: ClientOptions)

stamp(hash, options?): Promise<Buffer>

Submits the hash to configured calendars and returns a serialized .ots proof.

| Parameter | Type | Description | |---|---|---| | hash | Buffer \| string | SHA-256 hash (32-byte Buffer or 64-char hex string) | | options.signal | AbortSignal | Override the client-level signal for this call |

Throws ValidationError if the hash format is invalid.
Throws StampError if fewer than minimumSuccessfulSubmissions calendars accepted.

upgrade(proof, options?): Promise<Buffer>

Queries the calendars referenced in the proof for Bitcoin confirmations. Returns the updated proof if at least one calendar confirmed; otherwise throws UpgradeError.

| Parameter | Type | Description | |---|---|---| | proof | Buffer | Serialized .ots proof as returned by stamp() | | options.signal | AbortSignal | Override the client-level signal for this call |

Throws ValidationError if the proof is malformed.
Throws UpgradeError if no calendar has confirmed the timestamp yet.

verify(proof, originalDataHash?): Promise<VerificationResult>

Verifies a completed proof against the Bitcoin blockchain via Esplora. Never throws for invalid or incomplete proofs — failures are returned as { valid: false, error: '...' }.

| Parameter | Type | Description | |---|---|---| | proof | Buffer | Completed .ots proof with a Bitcoin attestation | | originalDataHash | Buffer \| string \| undefined | If provided, also checks that the proof was created for this hash |

Returns VerificationResult:

{
  valid: boolean
  blockHeight?: number   // Bitcoin block number
  blockHash?: string     // Block hash (hex)
  timestamp?: number     // Unix epoch of the block
  error?: string         // Set when valid is false
}

getCircuitState(calendarUrl): CircuitState | undefined

Returns the current state of the circuit breaker for a calendar URL ('CLOSED', 'OPEN', 'HALF_OPEN', or undefined if not yet initialized).

resetCircuit(calendarUrl): void

Manually resets the circuit breaker for a calendar. Use this after a known outage is resolved.

resetAllCircuits(): void

Resets all circuit breakers across all calendars.


Errors

All errors extend OpenTimestampsClientError extends Error.

| Class | When | |---|---| | ValidationError | Invalid input (bad hash format, malformed proof, invalid URL) | | StampError | stamp() did not reach minimumSuccessfulSubmissions. Has .successfulSubmissions and .failedSubmissions arrays | | UpgradeError | No calendar confirmed the timestamp yet | | NetworkError | Network failure (timeout, all retries exhausted). Has .status?: number | | CircuitBreakerError extends NetworkError | Request rejected because the circuit is OPEN | | CommitmentNotFoundError extends NetworkError | Calendar returned 404 for a commitment | | CalendarResponseTooLargeError extends NetworkError | Calendar response exceeded the 10 KB size limit | | EsploraResponseError extends NetworkError | Esplora returned an invalid, malformed, or oversized response |


Utility functions

hashFile(path): Promise<Buffer>

Returns the SHA-256 hash of a file as a 32-byte Buffer. Reads the file as a stream — safe for large files.

import { hashFile } from '@otskit/client'

const hash = await hashFile('contract.pdf')
const proof = await client.stamp(hash)

hashBuffer(data): Buffer

Returns the SHA-256 hash of a Buffer or Uint8Array synchronously.

import { hashBuffer } from '@otskit/client'

const hash = hashBuffer(Buffer.from('my data'))

Advanced exports

These are available for custom integrations and advanced use cases.

CalendarClient

Low-level client for a single OTS calendar server.

import { CalendarClient, ResilientNetworkLayer, DEFAULT_RESILIENCE } from '@otskit/client'

const network = new ResilientNetworkLayer(DEFAULT_RESILIENCE)
const calendar = new CalendarClient('https://alice.btc.calendar.opentimestamps.org', network)

const timestamp = await calendar.submit(digest)       // POST /digest
const upgraded  = await calendar.getTimestamp(digest) // GET /timestamp/:hex

EsploraClient

Client for querying a Bitcoin block explorer compatible with the Esplora API.

import { EsploraClient, ResilientNetworkLayer, DEFAULT_RESILIENCE, PUBLIC_ESPLORA_URL } from '@otskit/client'

const network = new ResilientNetworkLayer(DEFAULT_RESILIENCE)
const esplora = new EsploraClient(network, { url: PUBLIC_ESPLORA_URL })

const blockHash   = await esplora.blockHash(850_000)         // → hex string
const blockHeader = await esplora.block(blockHash)           // → { merkleroot, time }

verifyTimestampAttestation

Verifies a single Attestation (Bitcoin or Litecoin) against a block explorer.

import { verifyTimestampAttestation } from '@otskit/client'

const blockTime = await verifyTimestampAttestation(digest, attestation, esploraClient)

UrlWhitelist

Wildcard URL allowlist used internally to validate calendar URLs in upgrade proofs.

import { UrlWhitelist } from '@otskit/client'

const wl = new UrlWhitelist([
  'https://*.calendar.opentimestamps.org',
  'https://my-calendar.example.com',
])

wl.contains('https://alice.btc.calendar.opentimestamps.org') // true
wl.contains('https://evil.example.com')                      // false

ResilientNetworkLayer

The full timeout + retry + circuit-breaker stack as a standalone class.

import { ResilientNetworkLayer, DEFAULT_RESILIENCE } from '@otskit/client'

const network = new ResilientNetworkLayer(DEFAULT_RESILIENCE, logger)
const response = await network.request(calendarUrl, {
  url: 'https://...',
  method: 'POST',
  headers: { 'Content-Type': 'application/octet-stream' },
  body: new Uint8Array([...]),
})
// response.data: Uint8Array, response.ok: boolean, response.status: number

Constants

import {
  DEFAULT_CALENDARS,           // string[] — the four public OTS calendars
  DEFAULT_RESILIENCE,          // ResilienceOptions — default timeout/retry/cb config
  DEFAULT_CALENDAR_WHITELIST,  // UrlWhitelist — trusted calendar domains for upgrade
  DEFAULT_AGGREGATORS,         // string[] — OTS aggregator pool URLs
  PUBLIC_ESPLORA_URL,          // 'https://blockstream.info/api'
  MAX_CALENDAR_RESPONSE_SIZE,  // 10_000 (bytes)
  MAX_ESPLORA_RESPONSE_SIZE,   // 100_000 (bytes)
} from '@otskit/client'

Contributing

Contributions are welcome. Please open an issue before starting significant work so we can align on approach.

Setup

git clone https://github.com/OTSkit/OTSkit-client.git
cd OTSkit-client
npm install
npm test        # 160 unit + integration tests
npm run lint    # ESLint
npm run build   # tsup → dist/

Testing

The test suite uses Vitest, MSW for HTTP mocking, and fast-check for property-based testing. All tests run in Node.js (no browser required).

npm test                    # run all tests once
npm run test:watch          # watch mode
npm test -- --coverage      # with coverage report (100% threshold enforced)

Commit convention

This repository uses Conventional Commits. Releases are automated via semantic-release.

Code style

  • TypeScript strict mode
  • ESLint + Prettier (run npm run format before pushing)
  • Fail-closed: all external input is validated at the boundary
  • No runtime dependencies

Links

License

MIT