@otskit/client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 alexalves87
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,531 @@
1
+ <p align="center">
2
+ <img src=".github/otskit-client-header.png" alt="OTSkit Client" width="480" />
3
+ </p>
4
+
5
+ # @otskit/client
6
+
7
+ > TypeScript/JavaScript client for OpenTimestamps with enterprise-grade resilience patterns
8
+
9
+ [![CI](https://github.com/OTSkit/OTSkit-client/actions/workflows/ci.yml/badge.svg)](https://github.com/OTSkit/OTSkit-client/actions/workflows/ci.yml)
10
+ [![npm version](https://img.shields.io/npm/v/@otskit/client.svg)](https://www.npmjs.com/package/@otskit/client)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
12
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.6-blue.svg)](https://www.typescriptlang.org/)
13
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org/)
14
+
15
+ `@otskit/client` is the official client SDK for submitting, upgrading, and verifying [OpenTimestamps](https://opentimestamps.org) proofs. It sits on top of [@otskit/core](https://github.com/OTSkit/OTSkit-core) — the low-level protocol engine — and wraps it in a high-level API with production-ready resilience patterns built in.
16
+
17
+ ## Features
18
+
19
+ ### Complete OpenTimestamps Workflow
20
+ - **`stamp()`** — Hash your data, build a Merkle tree with a secure nonce, and submit to multiple calendar servers simultaneously
21
+ - **`upgrade()`** — Query calendars for Bitcoin confirmations and merge them into the pending proof
22
+ - **`verify()`** — Verify a completed proof against the Bitcoin blockchain via Esplora
23
+
24
+ ### Enterprise-Grade Resilience
25
+ - **Circuit Breaker** — Per-calendar isolation; one failing calendar never affects the others
26
+ - **Exponential Backoff** — Three strategies (`exponential`, `linear`, `constant`) with three jitter modes (`full`, `equal`, `none`)
27
+ - **Dual Timeouts** — Independent `totalTimeoutMs` (whole operation) and `connectTimeoutMs` (per attempt)
28
+ - **Threshold Submissions** — `stamp()` requires N-of-M successful submissions (default 2-of-4); configurable
29
+ - **Fail-Fast on 4xx** — Client errors are never retried; only 5xx and network failures trigger retries
30
+
31
+ ### Developer Experience
32
+ - **TypeScript-first** — Strict types throughout; full IntelliSense for every option and error
33
+ - **Multi-runtime** — Node.js 18+, browsers, and edge runtimes (uses the standard `fetch` API)
34
+ - **Tree-shakeable** — Dual ESM/CJS build, zero runtime dependencies
35
+ - **`AbortController` support** — Cancel any in-flight operation at any level
36
+ - **Observable** — Drop-in `Logger` interface compatible with `console`, `pino`, `winston`, etc.
37
+
38
+ ---
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ npm install @otskit/client
44
+ ```
45
+
46
+ `@otskit/core` is a peer dependency bundled as a `file:` reference in monorepo setups; no separate install is needed.
47
+
48
+ ---
49
+
50
+ ## Quick Start
51
+
52
+ ```typescript
53
+ import { OpenTimestampsClient } from '@otskit/client'
54
+ import { createHash } from 'crypto'
55
+ import { readFileSync, writeFileSync } from 'fs'
56
+
57
+ const client = new OpenTimestampsClient()
58
+
59
+ // 1. Hash the file you want to timestamp
60
+ const fileBytes = readFileSync('contract.pdf')
61
+ const hash = createHash('sha256').update(fileBytes).digest()
62
+
63
+ // 2. Submit to calendars → get a pending .ots proof
64
+ const pendingProof = await client.stamp(hash)
65
+ writeFileSync('contract.pdf.ots', pendingProof)
66
+ console.log('Proof saved — Bitcoin confirmation usually arrives in 10–60 minutes.')
67
+
68
+ // 3. Later: query calendars for a Bitcoin confirmation
69
+ const upgradedProof = await client.upgrade(pendingProof)
70
+ writeFileSync('contract.pdf.ots', upgradedProof)
71
+
72
+ // 4. Verify the completed proof
73
+ const result = await client.verify(upgradedProof, hash)
74
+ if (result.valid) {
75
+ console.log(`Timestamp confirmed in Bitcoin block ${result.blockHeight}`)
76
+ console.log(`Block time: ${new Date(result.timestamp! * 1000).toISOString()}`)
77
+ } else {
78
+ console.error(`Verification failed: ${result.error}`)
79
+ }
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Usage
85
+
86
+ ### Stamping data
87
+
88
+ `stamp()` accepts either a 32-byte `Buffer` or a 64-character hex string:
89
+
90
+ ```typescript
91
+ import { createHash } from 'crypto'
92
+
93
+ // From a Buffer
94
+ const hashBuffer = createHash('sha256').update(fileBytes).digest()
95
+ const proof = await client.stamp(hashBuffer)
96
+
97
+ // From a hex string
98
+ const hashHex = createHash('sha256').update(fileBytes).digest('hex')
99
+ const proof = await client.stamp(hashHex)
100
+ ```
101
+
102
+ 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.
103
+
104
+ ### Upgrading a pending proof
105
+
106
+ 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.
107
+
108
+ ```typescript
109
+ import { UpgradeError } from '@otskit/client'
110
+
111
+ try {
112
+ const upgradedProof = await client.upgrade(pendingProof)
113
+ // Save and stop polling
114
+ } catch (err) {
115
+ if (err instanceof UpgradeError) {
116
+ // No calendar has a Bitcoin confirmation yet — try again later
117
+ console.log('Not confirmed yet, retry in 5 minutes')
118
+ }
119
+ }
120
+ ```
121
+
122
+ ### Verifying a proof
123
+
124
+ `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.
125
+
126
+ ```typescript
127
+ const result = await client.verify(proof, originalHash)
128
+
129
+ if (result.valid) {
130
+ console.log(result.blockHeight) // Bitcoin block number
131
+ console.log(result.blockHash) // Block hash (hex)
132
+ console.log(result.timestamp) // Unix timestamp of the block
133
+ } else {
134
+ console.log(result.error) // Human-readable reason
135
+ }
136
+ ```
137
+
138
+ `verify()` always returns `VerificationResult` — it never throws for invalid proofs, only for unexpected network failures.
139
+
140
+ ### Error handling
141
+
142
+ ```typescript
143
+ import {
144
+ StampError,
145
+ UpgradeError,
146
+ ValidationError,
147
+ NetworkError,
148
+ CircuitBreakerError,
149
+ } from '@otskit/client'
150
+
151
+ try {
152
+ await client.stamp(hash)
153
+ } catch (err) {
154
+ if (err instanceof ValidationError) {
155
+ // Invalid hash format
156
+ } else if (err instanceof StampError) {
157
+ // Not enough calendars accepted the submission
158
+ console.log(`Succeeded: ${err.successfulSubmissions.map(s => s.calendar)}`)
159
+ console.log(`Failed: ${err.failedSubmissions.map(s => s.calendar)}`)
160
+ } else if (err instanceof CircuitBreakerError) {
161
+ // A calendar is isolated due to repeated failures
162
+ } else if (err instanceof NetworkError) {
163
+ console.log(`HTTP status: ${err.status}`) // undefined for non-HTTP errors
164
+ }
165
+ }
166
+ ```
167
+
168
+ ### Cancellation with AbortController
169
+
170
+ You can cancel individual operations or set a client-wide signal:
171
+
172
+ ```typescript
173
+ // Per-operation cancellation
174
+ const controller = new AbortController()
175
+ setTimeout(() => controller.abort(), 10_000)
176
+
177
+ const proof = await client.stamp(hash, { signal: controller.signal })
178
+
179
+ // Client-wide cancellation (applies to all operations)
180
+ const clientController = new AbortController()
181
+ const client = new OpenTimestampsClient({ signal: clientController.signal })
182
+
183
+ clientController.abort() // cancels any in-flight request
184
+ ```
185
+
186
+ ### Observability with a logger
187
+
188
+ Any object with `debug`, `info`, `warn`, and `error` methods works:
189
+
190
+ ```typescript
191
+ import pino from 'pino'
192
+
193
+ const client = new OpenTimestampsClient({
194
+ logger: pino({ level: 'debug' }),
195
+ })
196
+ ```
197
+
198
+ Using `console` directly:
199
+
200
+ ```typescript
201
+ const client = new OpenTimestampsClient({ logger: console })
202
+ ```
203
+
204
+ ### Monitoring circuit breakers
205
+
206
+ ```typescript
207
+ const state = client.getCircuitState('https://alice.btc.calendar.opentimestamps.org')
208
+ // 'CLOSED' | 'OPEN' | 'HALF_OPEN' | undefined
209
+
210
+ // Manually recover a calendar after a known incident
211
+ client.resetCircuit('https://alice.btc.calendar.opentimestamps.org')
212
+
213
+ // Reset all calendars at once
214
+ client.resetAllCircuits()
215
+ ```
216
+
217
+ ---
218
+
219
+ ## Configuration
220
+
221
+ ### `ClientOptions`
222
+
223
+ ```typescript
224
+ const client = new OpenTimestampsClient({
225
+ // Calendar servers to submit to (default: the four public OTS calendars)
226
+ calendars: [
227
+ 'https://alice.btc.calendar.opentimestamps.org',
228
+ 'https://bob.btc.calendar.opentimestamps.org',
229
+ 'https://finney.calendar.eternitywall.com',
230
+ 'https://btc.calendar.catallaxy.com',
231
+ ],
232
+
233
+ // How many calendars must succeed for stamp() to resolve (default: 2)
234
+ minimumSuccessfulSubmissions: 2,
235
+
236
+ // Resilience configuration (see below)
237
+ resilience: { ... },
238
+
239
+ // Logger implementing { debug, info, warn, error }
240
+ logger: console,
241
+
242
+ // AbortSignal applied to all operations on this client
243
+ signal: controller.signal,
244
+ })
245
+ ```
246
+
247
+ ### `ResilienceOptions`
248
+
249
+ All fields are optional — unspecified fields fall back to the defaults shown.
250
+
251
+ ```typescript
252
+ resilience: {
253
+ // Maximum total time for a single operation across all retries (ms)
254
+ totalTimeoutMs: 30_000, // default
255
+
256
+ // Maximum time for a single HTTP attempt (ms)
257
+ connectTimeoutMs: 5_000, // default
258
+
259
+ retries: {
260
+ enabled: true, // default
261
+ maxAttempts: 3, // default
262
+
263
+ backoff: {
264
+ strategy: 'exponential', // 'exponential' | 'linear' | 'constant'
265
+ initialDelayMs: 200, // default
266
+ maxDelayMs: 5_000, // default; caps the computed delay
267
+ jitter: 'full', // 'full' | 'equal' | 'none'
268
+ },
269
+ },
270
+
271
+ circuitBreaker: {
272
+ enabled: true, // default
273
+ failureThreshold: 5, // consecutive failures before OPEN (default)
274
+ recoveryTimeoutMs: 15_000,// time in OPEN before trying HALF_OPEN (default)
275
+ halfOpenMaxAttempts: 1, // probing requests in HALF_OPEN state (default)
276
+ },
277
+ }
278
+ ```
279
+
280
+ **Backoff strategies:**
281
+
282
+ | Strategy | Delay formula |
283
+ |---|---|
284
+ | `exponential` | `initialDelayMs × 2^(attempt - 1)` |
285
+ | `linear` | `initialDelayMs × attempt` |
286
+ | `constant` | `initialDelayMs` |
287
+
288
+ **Jitter modes:**
289
+
290
+ | Mode | Effect |
291
+ |---|---|
292
+ | `full` | Random value in `[0, delay]` — best for thundering-herd prevention |
293
+ | `equal` | Random value in `[delay/2, delay]` |
294
+ | `none` | Deterministic delay |
295
+
296
+ **Circuit breaker states:**
297
+
298
+ ```
299
+ CLOSED ──(failureThreshold consecutive failures)──► OPEN
300
+ OPEN ──(recoveryTimeoutMs elapsed) ──► HALF_OPEN
301
+ HALF_OPEN ──(success) ──► CLOSED
302
+ HALF_OPEN ──(failure) ──► OPEN
303
+ ```
304
+
305
+ ---
306
+
307
+ ## API Reference
308
+
309
+ ### `OpenTimestampsClient`
310
+
311
+ #### Constructor
312
+
313
+ ```typescript
314
+ new OpenTimestampsClient(options?: ClientOptions)
315
+ ```
316
+
317
+ #### `stamp(hash, options?): Promise<Buffer>`
318
+
319
+ Submits the hash to configured calendars and returns a serialized `.ots` proof.
320
+
321
+ | Parameter | Type | Description |
322
+ |---|---|---|
323
+ | `hash` | `Buffer \| string` | SHA-256 hash (32-byte Buffer or 64-char hex string) |
324
+ | `options.signal` | `AbortSignal` | Override the client-level signal for this call |
325
+
326
+ Throws `ValidationError` if the hash format is invalid.
327
+ Throws `StampError` if fewer than `minimumSuccessfulSubmissions` calendars accepted.
328
+
329
+ #### `upgrade(proof, options?): Promise<Buffer>`
330
+
331
+ Queries the calendars referenced in the proof for Bitcoin confirmations. Returns the updated proof if at least one calendar confirmed; otherwise throws `UpgradeError`.
332
+
333
+ | Parameter | Type | Description |
334
+ |---|---|---|
335
+ | `proof` | `Buffer` | Serialized `.ots` proof as returned by `stamp()` |
336
+ | `options.signal` | `AbortSignal` | Override the client-level signal for this call |
337
+
338
+ Throws `ValidationError` if the proof is malformed.
339
+ Throws `UpgradeError` if no calendar has confirmed the timestamp yet.
340
+
341
+ #### `verify(proof, originalDataHash?): Promise<VerificationResult>`
342
+
343
+ Verifies a completed proof against the Bitcoin blockchain via Esplora. Never throws for invalid or incomplete proofs — failures are returned as `{ valid: false, error: '...' }`.
344
+
345
+ | Parameter | Type | Description |
346
+ |---|---|---|
347
+ | `proof` | `Buffer` | Completed `.ots` proof with a Bitcoin attestation |
348
+ | `originalDataHash` | `Buffer \| string \| undefined` | If provided, also checks that the proof was created for this hash |
349
+
350
+ Returns `VerificationResult`:
351
+
352
+ ```typescript
353
+ {
354
+ valid: boolean
355
+ blockHeight?: number // Bitcoin block number
356
+ blockHash?: string // Block hash (hex)
357
+ timestamp?: number // Unix epoch of the block
358
+ error?: string // Set when valid is false
359
+ }
360
+ ```
361
+
362
+ #### `getCircuitState(calendarUrl): CircuitState | undefined`
363
+
364
+ Returns the current state of the circuit breaker for a calendar URL (`'CLOSED'`, `'OPEN'`, `'HALF_OPEN'`, or `undefined` if not yet initialized).
365
+
366
+ #### `resetCircuit(calendarUrl): void`
367
+
368
+ Manually resets the circuit breaker for a calendar. Use this after a known outage is resolved.
369
+
370
+ #### `resetAllCircuits(): void`
371
+
372
+ Resets all circuit breakers across all calendars.
373
+
374
+ ---
375
+
376
+ ### Errors
377
+
378
+ All errors extend `OpenTimestampsClientError extends Error`.
379
+
380
+ | Class | When |
381
+ |---|---|
382
+ | `ValidationError` | Invalid input (bad hash format, malformed proof, invalid URL) |
383
+ | `StampError` | `stamp()` did not reach `minimumSuccessfulSubmissions`. Has `.successfulSubmissions` and `.failedSubmissions` arrays |
384
+ | `UpgradeError` | No calendar confirmed the timestamp yet |
385
+ | `NetworkError` | Network failure (timeout, all retries exhausted). Has `.status?: number` |
386
+ | `CircuitBreakerError extends NetworkError` | Request rejected because the circuit is OPEN |
387
+ | `CommitmentNotFoundError extends NetworkError` | Calendar returned 404 for a commitment |
388
+ | `CalendarResponseTooLargeError extends NetworkError` | Calendar response exceeded the 10 KB size limit |
389
+ | `EsploraResponseError extends NetworkError` | Esplora returned an invalid, malformed, or oversized response |
390
+
391
+ ---
392
+
393
+ ### Advanced exports
394
+
395
+ These are available for custom integrations and advanced use cases.
396
+
397
+ #### `CalendarClient`
398
+
399
+ Low-level client for a single OTS calendar server.
400
+
401
+ ```typescript
402
+ import { CalendarClient, ResilientNetworkLayer, DEFAULT_RESILIENCE } from '@otskit/client'
403
+
404
+ const network = new ResilientNetworkLayer(DEFAULT_RESILIENCE)
405
+ const calendar = new CalendarClient('https://alice.btc.calendar.opentimestamps.org', network)
406
+
407
+ const timestamp = await calendar.submit(digest) // POST /digest
408
+ const upgraded = await calendar.getTimestamp(digest) // GET /timestamp/:hex
409
+ ```
410
+
411
+ #### `EsploraClient`
412
+
413
+ Client for querying a Bitcoin block explorer compatible with the [Esplora API](https://github.com/Blockstream/esplora/blob/master/API.md).
414
+
415
+ ```typescript
416
+ import { EsploraClient, ResilientNetworkLayer, DEFAULT_RESILIENCE, PUBLIC_ESPLORA_URL } from '@otskit/client'
417
+
418
+ const network = new ResilientNetworkLayer(DEFAULT_RESILIENCE)
419
+ const esplora = new EsploraClient(network, { url: PUBLIC_ESPLORA_URL })
420
+
421
+ const blockHash = await esplora.blockHash(850_000) // → hex string
422
+ const blockHeader = await esplora.block(blockHash) // → { merkleroot, time }
423
+ ```
424
+
425
+ #### `verifyTimestampAttestation`
426
+
427
+ Verifies a single `Attestation` (Bitcoin or Litecoin) against a block explorer.
428
+
429
+ ```typescript
430
+ import { verifyTimestampAttestation } from '@otskit/client'
431
+
432
+ const blockTime = await verifyTimestampAttestation(digest, attestation, esploraClient)
433
+ ```
434
+
435
+ #### `UrlWhitelist`
436
+
437
+ Wildcard URL allowlist used internally to validate calendar URLs in upgrade proofs.
438
+
439
+ ```typescript
440
+ import { UrlWhitelist } from '@otskit/client'
441
+
442
+ const wl = new UrlWhitelist([
443
+ 'https://*.calendar.opentimestamps.org',
444
+ 'https://my-calendar.example.com',
445
+ ])
446
+
447
+ wl.contains('https://alice.btc.calendar.opentimestamps.org') // true
448
+ wl.contains('https://evil.example.com') // false
449
+ ```
450
+
451
+ #### `ResilientNetworkLayer`
452
+
453
+ The full timeout + retry + circuit-breaker stack as a standalone class.
454
+
455
+ ```typescript
456
+ import { ResilientNetworkLayer, DEFAULT_RESILIENCE } from '@otskit/client'
457
+
458
+ const network = new ResilientNetworkLayer(DEFAULT_RESILIENCE, logger)
459
+ const response = await network.request(calendarUrl, {
460
+ url: 'https://...',
461
+ method: 'POST',
462
+ headers: { 'Content-Type': 'application/octet-stream' },
463
+ body: new Uint8Array([...]),
464
+ })
465
+ // response.data: Uint8Array, response.ok: boolean, response.status: number
466
+ ```
467
+
468
+ #### Constants
469
+
470
+ ```typescript
471
+ import {
472
+ DEFAULT_CALENDARS, // string[] — the four public OTS calendars
473
+ DEFAULT_RESILIENCE, // ResilienceOptions — default timeout/retry/cb config
474
+ DEFAULT_CALENDAR_WHITELIST, // UrlWhitelist — trusted calendar domains for upgrade
475
+ DEFAULT_AGGREGATORS, // string[] — OTS aggregator pool URLs
476
+ PUBLIC_ESPLORA_URL, // 'https://blockstream.info/api'
477
+ MAX_CALENDAR_RESPONSE_SIZE, // 10_000 (bytes)
478
+ MAX_ESPLORA_RESPONSE_SIZE, // 100_000 (bytes)
479
+ } from '@otskit/client'
480
+ ```
481
+
482
+ ---
483
+
484
+ ## Contributing
485
+
486
+ Contributions are welcome. Please open an issue before starting significant work so we can align on approach.
487
+
488
+ ### Setup
489
+
490
+ ```bash
491
+ git clone https://github.com/OTSkit/OTSkit-client.git
492
+ cd OTSkit-client
493
+ npm install
494
+ npm test # 160 unit + integration tests
495
+ npm run lint # ESLint
496
+ npm run build # tsup → dist/
497
+ ```
498
+
499
+ ### Testing
500
+
501
+ The test suite uses [Vitest](https://vitest.dev), [MSW](https://mswjs.io) for HTTP mocking, and [fast-check](https://fast-check.dev) for property-based testing. All tests run in Node.js (no browser required).
502
+
503
+ ```bash
504
+ npm test # run all tests once
505
+ npm run test:watch # watch mode
506
+ npm test -- --coverage # with coverage report (100% threshold enforced)
507
+ ```
508
+
509
+ ### Commit convention
510
+
511
+ This repository uses [Conventional Commits](https://www.conventionalcommits.org). Releases are automated via [semantic-release](https://semantic-release.gitbook.io).
512
+
513
+ ### Code style
514
+
515
+ - TypeScript strict mode
516
+ - ESLint + Prettier (run `npm run format` before pushing)
517
+ - Fail-closed: all external input is validated at the boundary
518
+ - No runtime dependencies
519
+
520
+ ---
521
+
522
+ ## Links
523
+
524
+ - [OpenTimestamps Protocol](https://opentimestamps.org)
525
+ - [@otskit/core](https://github.com/OTSkit/OTSkit-core) — Protocol engine used by this SDK
526
+ - [npm Package](https://www.npmjs.com/package/@otskit/client)
527
+ - [Issue Tracker](https://github.com/OTSkit/OTSkit-client/issues)
528
+
529
+ ## License
530
+
531
+ MIT