
Security News
Ruby's Bundler 4.0.18 Extends Cooldown to bundle lock and bundle cache
The supply chain control that delays freshly published gems now covers lockfile generation and gem vendoring in Ruby projects.
@otskit/client
Advanced tools
TypeScript/JavaScript client for OpenTimestamps with enterprise-grade resilience patterns
@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.
stamp() — Hash your data, build a Merkle tree with a secure nonce, and submit to multiple calendar servers simultaneouslyupgrade() — Query calendars for Bitcoin confirmations and merge them into the pending proofverify() — Verify a completed proof against the Bitcoin blockchain via Esploraexponential, linear, constant) with three jitter modes (full, equal, none)totalTimeoutMs (whole operation) and connectTimeoutMs (per attempt)stamp() requires N-of-M successful submissions (default 2-of-4); configurablecrypto, dns, and net APIs not available in browsers or edge runtimesAbortController support — Cancel any in-flight operation at any levelLogger interface compatible with console, pino, winston, etc.hashFile() and hashBuffer() so you don't need to wire up crypto yourselfNote on confirmation times: After
stamp(), the proof ispending— 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. Callupgrade()periodically to check; anUpgradeErrorsimply means the blockchain hasn't confirmed yet, not that anything went wrong.
npm install @otskit/client
@otskit/core is a peer dependency bundled as a file: reference in monorepo setups; no separate install is needed.
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}`)
}
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().
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.
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')
}
}
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.
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
}
}
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
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 })
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()
ClientOptionsconst 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,
})
ResilienceOptionsAll 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
OpenTimestampsClientnew 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 | undefinedReturns the current state of the circuit breaker for a calendar URL ('CLOSED', 'OPEN', 'HALF_OPEN', or undefined if not yet initialized).
resetCircuit(calendarUrl): voidManually resets the circuit breaker for a calendar. Use this after a known outage is resolved.
resetAllCircuits(): voidResets all circuit breakers across all calendars.
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 |
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): BufferReturns the SHA-256 hash of a Buffer or Uint8Array synchronously.
import { hashBuffer } from '@otskit/client'
const hash = hashBuffer(Buffer.from('my data'))
These are available for custom integrations and advanced use cases.
CalendarClientLow-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
EsploraClientClient 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 }
verifyTimestampAttestationVerifies a single Attestation (Bitcoin or Litecoin) against a block explorer.
import { verifyTimestampAttestation } from '@otskit/client'
const blockTime = await verifyTimestampAttestation(digest, attestation, esploraClient)
UrlWhitelistWildcard 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
ResilientNetworkLayerThe 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
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'
Contributions are welcome. Please open an issue before starting significant work so we can align on approach.
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/
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)
This repository uses Conventional Commits. Releases are automated via semantic-release.
npm run format before pushing)MIT
FAQs
Official client SDK for OpenTimestamps calendars with resilience patterns
We found that @otskit/client demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
The supply chain control that delays freshly published gems now covers lockfile generation and gem vendoring in Ruby projects.

Security News
During a UK cyber test, a Mythos 5 agent used sockpuppets, social engineering, and prompt injection to try to get a maintainer to merge malware.

Company News
Socket is now in the AWS Security Hub Extended plan. Adopt it through AWS, apply committed spend, and block malicious open source packages.