@otskit/core

OpenTimestamps core library — TypeScript, zero-dependency, fail-closed.
The next generation of @alexalves87/opentimestamps, rewritten from the ground up with TypeScript 6 strict mode, zero external dependencies, and a fail-closed security posture.
What is OpenTimestamps?
OpenTimestamps is an open standard for decentralized timestamp proofs. It lets you prove that a document existed at a specific point in time by anchoring its cryptographic hash into the Bitcoin (or Litecoin) blockchain. The proof is stored in a compact .ots file — no trusted third party required for verification, ever.
Features
- Zero dependencies — pure TypeScript implementation, no npm supply chain risk
- Fail-closed by design — strict input validation; rejects
ArrayBuffer, Buffer, and any malformed input by default
- TypeScript-first — full type declarations, TypeScript 6 strict mode, ESM + CJS dual build
- Complete protocol — create, serialize, deserialize, merge, and verify timestamps
- Merkle tree support — batch-timestamp thousands of documents in a single blockchain transaction
- Pure crypto — built-in SHA1, SHA256, and RIPEMD-160 with no native bindings
Installation
npm install @otskit/core
Requires Node.js ≥ 20.
Quick Start
Timestamp a file
import { DetachedTimestampFile, OpSHA256, makePending } from '@otskit/core';
import { readFileSync, writeFileSync } from 'node:fs';
const fileContent = new Uint8Array(readFileSync('document.pdf'));
const dtf = DetachedTimestampFile.fromBytes(new OpSHA256(), fileContent);
dtf.timestamp.attestations.push(
makePending('https://alice.btc.calendar.opentimestamps.org'),
);
writeFileSync('document.pdf.ots', dtf.serializeToBytes());
Read a .ots file
import { DetachedTimestampFile } from '@otskit/core';
import { readFileSync } from 'node:fs';
const otsBytes = new Uint8Array(readFileSync('document.pdf.ots'));
const dtf = DetachedTimestampFile.deserialize(otsBytes);
console.log('Hash algorithm:', dtf.fileHashOp.tagName);
console.log('File digest: ', Buffer.from(dtf.fileDigest()).toString('hex'));
console.log('Complete: ', dtf.timestamp.isTimestampComplete());
console.log('Attestations: ', dtf.timestamp.getAttestations());
import { DetachedTimestampFile, verifyAgainstBlockheader } from '@otskit/core';
import { readFileSync } from 'node:fs';
const dtf = DetachedTimestampFile.deserialize(
new Uint8Array(readFileSync('document.pdf.ots')),
);
for (const { msg, attestation } of dtf.timestamp.allAttestations()) {
if (attestation.kind === 'bitcoin') {
verifyAgainstBlockheader(msg, blockHeader);
console.log(`Verified at Bitcoin block height ${attestation.height}`);
}
}
Batch-timestamp with a Merkle tree
Anchor thousands of documents in a single blockchain transaction:
import { DetachedTimestampFile, OpSHA256, makeMerkleTree, makePending } from '@otskit/core';
import { readFileSync, writeFileSync } from 'node:fs';
const files = ['a.pdf', 'b.pdf', 'c.pdf'];
const op = new OpSHA256();
const dtfs = files.map(f =>
DetachedTimestampFile.fromBytes(op, new Uint8Array(readFileSync(f))),
);
const root = makeMerkleTree(dtfs.map(d => d.timestamp));
root.attestations.push(makePending('https://alice.btc.calendar.opentimestamps.org'));
files.forEach((f, i) => writeFileSync(`${f}.ots`, dtfs[i]!.serializeToBytes()));
API Reference
DetachedTimestampFile
Immutable wrapper for a .ots proof file.
DetachedTimestampFile.fromBytes(op, content) | Create from raw file bytes |
DetachedTimestampFile.fromHash(op, digest) | Create from a pre-computed digest |
DetachedTimestampFile.deserialize(bytes) | Parse a .ots file (Uint8Array only — fail-closed) |
.serializeToBytes() | Serialize back to .ots bytes |
.fileDigest() | The file's hash as Uint8Array (defensive copy) |
.fileHashOp | The CryptOp used to hash the file |
.timestamp | The root Timestamp of the proof tree |
Timestamp
A node in the proof tree. Each node holds a digest (msg), direct attestations, and operation branches leading to sub-timestamps.
new Timestamp(msg) | Create a new leaf node |
.add(op) | Apply an operation and return (or reuse) the sub-timestamp |
.addExisting(op, stamp) | Cross-link to an existing timestamp (used internally by Merkle) |
.merge(other) | Absorb attestations and branches from another timestamp with the same msg |
.attestations | Direct Attestation[] — push to add seals to this node |
.getDigest() | Defensive copy of msg |
.getAttestations() | All attestations anywhere in the tree |
.allAttestations() | All { msg, attestation } pairs in the tree |
.isTimestampComplete() | true if a Bitcoin or Litecoin attestation exists |
.allTips() | The leaf digests (nodes with no operations) |
.equals(other) | Deep structural equality |
Operations
All operations extend Op and are serializable. Binary ops (OpAppend, OpPrepend) take a Uint8Array argument in their constructor.
OpSHA256 | 0x08 | SHA-256 hash |
OpSHA1 | 0x02 | SHA-1 hash |
OpRIPEMD160 | 0x03 | RIPEMD-160 hash |
OpAppend(suffix) | 0xf0 | Append bytes |
OpPrepend(prefix) | 0xf1 | Prepend bytes |
OpReverse | 0xf2 | Reverse byte order |
OpHexlify | 0xf3 | Encode to ASCII hex |
Attestations
makePending(uri) | Pending calendar attestation — proof not yet upgraded to blockchain |
makeBitcoin(height) | Bitcoin blockchain attestation at block height |
makeLitecoin(height) | Litecoin blockchain attestation at block height |
verifyAgainstBlockheader(digest, header) | Verify a 32-byte digest against a block's Merkle root — throws VerificationError on failure |
Merkle tree
makeMerkleTree(timestamps) | Build a Merkle-Mountain-Range from an array of timestamps. Returns the root. Throws EmptyMerkleTreeError on empty input. |
Serialization contexts
For low-level binary I/O following the OpenTimestamps wire format (LEB128 varints, length-prefixed bytes):
StreamSerializationContext | Write binary data; call .getOutput() for the result |
StreamDeserializationContext | Read binary data with bounds checking and EOF enforcement |
Utilities
hexToBytes(hex) | Hex string → Uint8Array |
bytesToHex(bytes) | Uint8Array → lowercase hex string |
textToBytes(text) | UTF-8 string → Uint8Array |
bytesToText(bytes) | Uint8Array → UTF-8 string (throws on invalid sequences) |
randBytes(n) | Cryptographically secure random bytes via crypto.getRandomValues — throws if unavailable |
Errors
All error classes extend Error and are exported by name for precise catch handling.
| Deserialization | DeserializationError, BadMagicError, TruncatedStreamError, OversizedDataError, VaruintOverflowError, TrailingGarbageError, UnknownOperationError, InvalidUriError, UnsupportedVersionError |
| Operation | OpExecutionError, MessageTooLongError, ResultTooLongError |
| Verification | VerificationError |
| Tree | EmptyTimestampError, MergeError, EmptyMerkleTreeError |
Contributing
git clone https://github.com/OTSkit/OTSkit-core.git
cd OTSkit-core
npm install
npm test
npm run build
npm run coverage
All contributions must preserve the fail-closed philosophy: strict input validation at every boundary, no silent failures, no implicit type coercion.
License
MIT © 2026 OTSkit contributors — see LICENSE.