UNPKG

@basestamp/basestamp

Version:

TypeScript client library for Basestamp API with trustless Merkle proof verification

337 lines (330 loc) 12.1 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { BasestampClient: () => BasestampClient, BasestampError: () => BasestampError, MerkleProof: () => MerkleProof, Stamp: () => Stamp, calculateSHA256: () => calculateSHA256, verifyMerkleProof: () => verifyMerkleProof }); module.exports = __toCommonJS(src_exports); // src/client.ts var import_cross_fetch = __toESM(require("cross-fetch")); // src/types.ts var MerkleProof = class { constructor(data, verifier, hasher) { this.leaf_hash = data.leaf_hash; this.leaf_index = data.leaf_index; this.siblings = data.siblings; this.directions = data.directions; this.root_hash = data.root_hash; this.nonce = data.nonce; this.original_hash = data.original_hash; this._verifier = verifier; this._hasher = hasher; } verify(hash_value) { if (hash_value !== this.original_hash) { throw new BasestampError(`Hash mismatch: provided hash '${hash_value}' does not match proof's original hash '${this.original_hash}'`); } if (this.nonce) { const expectedLeafHash = this._hasher(this.nonce + this.original_hash); if (expectedLeafHash !== this.leaf_hash) { throw new BasestampError(`Leaf hash verification failed: expected '${expectedLeafHash}' but merkle proof contains '${this.leaf_hash}'. This indicates the nonce '${this.nonce}' and original hash '${this.original_hash}' do not match the merkle proof.`); } } else { if (this.original_hash !== this.leaf_hash) { throw new BasestampError(`Legacy verification failed: original hash '${this.original_hash}' does not match merkle proof leaf hash '${this.leaf_hash}'`); } } const isValidProof = this._verifier(this); if (!isValidProof) { throw new BasestampError(`Merkle proof verification failed: the proof structure (leaf_index: ${this.leaf_index}, siblings: [${this.siblings.join(", ")}], directions: [${this.directions.join(", ")}]) does not produce the expected root hash '${this.root_hash}'`); } return true; } }; var Stamp = class { constructor(data, verifier, hasher) { this.stamp_id = data.stamp_id; this.hash = data.hash; this.original_hash = data.original_hash || data.hash; this.nonce = data.nonce || ""; this.timestamp = data.timestamp; this.status = data.status; this.message = data.message; this.tx_id = data.tx_id; this.block_hash = data.block_hash; this.network = data.network; this.chain_id = data.chain_id; this.merkle_proof = data.merkle_proof; this._verifier = verifier; this._hasher = hasher; } verify(original_hash) { if (!this.merkle_proof) { throw new BasestampError("Merkle proof not available for verification"); } if (original_hash !== this.original_hash) { throw new BasestampError(`Hash mismatch: provided hash '${original_hash}' does not match stamp's original hash '${this.original_hash}'`); } if (this.nonce) { const expectedLeafHash = this._hasher(this.nonce + this.original_hash); if (expectedLeafHash !== this.merkle_proof.leaf_hash) { throw new BasestampError(`Leaf hash verification failed: expected '${expectedLeafHash}' but merkle proof contains '${this.merkle_proof.leaf_hash}'. This indicates the nonce '${this.nonce}' and original hash '${this.original_hash}' do not match the merkle proof.`); } } else { if (this.original_hash !== this.merkle_proof.leaf_hash) { throw new BasestampError(`Legacy verification failed: original hash '${this.original_hash}' does not match merkle proof leaf hash '${this.merkle_proof.leaf_hash}'`); } } const isValidProof = this._verifier(this.merkle_proof); if (!isValidProof) { throw new BasestampError(`Merkle proof verification failed: the proof structure (leaf_index: ${this.merkle_proof.leaf_index}, siblings: [${this.merkle_proof.siblings.join(", ")}], directions: [${this.merkle_proof.directions.join(", ")}]) does not produce the expected root hash '${this.merkle_proof.root_hash}'`); } return true; } getMerkleProof() { if (!this.merkle_proof) { throw new BasestampError("Merkle proof not available"); } return new MerkleProof({ ...this.merkle_proof, nonce: this.nonce, original_hash: this.original_hash }, this._verifier, this._hasher); } }; var BasestampError = class extends Error { constructor(message) { super(message); this.name = "BasestampError"; } }; // src/crypto-utils.ts var import_crypto_js = require("crypto-js"); function createHash(algorithm) { if (algorithm !== "sha256") { throw new Error(`Unsupported hash algorithm: ${algorithm}`); } return { update(data) { let input; if (Buffer.isBuffer(data)) { input = data.toString("hex"); const wordArray = import_crypto_js.enc.Hex.parse(input); this._hash = (0, import_crypto_js.SHA256)(wordArray); } else { this._hash = (0, import_crypto_js.SHA256)(data); } return this; }, digest(encoding) { if (!this._hash) { throw new Error("Hash not computed yet"); } if (encoding === "hex") { return this._hash.toString(import_crypto_js.enc.Hex); } else { throw new Error(`Unsupported encoding: ${encoding}`); } }, _hash: null }; } // src/merkle.ts var import_buffer = require("buffer"); function hashPair(left, right) { const leftBytes = import_buffer.Buffer.from(left, "hex"); const rightBytes = import_buffer.Buffer.from(right, "hex"); let combined; if (leftBytes.length === rightBytes.length) { if (left < right) { combined = import_buffer.Buffer.concat([leftBytes, rightBytes]); } else { combined = import_buffer.Buffer.concat([rightBytes, leftBytes]); } } else { combined = import_buffer.Buffer.concat([leftBytes, rightBytes]); } return createHash("sha256").update(combined).digest("hex"); } function verifyMerkleProof(proof) { if (!proof) { return false; } if (!proof.leaf_hash || !proof.root_hash) { return false; } if (proof.siblings.length !== proof.directions.length) { return false; } let currentHash = proof.leaf_hash; for (let i = 0; i < proof.siblings.length; i++) { const sibling = proof.siblings[i]; const direction = proof.directions[i]; if (direction) { currentHash = hashPair(currentHash, sibling); } else { currentHash = hashPair(sibling, currentHash); } } return currentHash === proof.root_hash; } function calculateSHA256(data) { const buffer = typeof data === "string" ? import_buffer.Buffer.from(data, "utf8") : data; return createHash("sha256").update(buffer).digest("hex"); } // src/client.ts var BasestampClient = class { constructor(options = {}) { this.baseURL = options.baseURL || "https://api.basestamp.io"; this.timeout = options.timeout || 3e4; } async submitSHA256(hash) { const request = { hash }; const response = await this.makeRequest("POST", "/stamp", request); if ("stamp_id" in response) { return response.stamp_id; } return response.hash; } async getStamp(stampId, options = {}) { const { wait = false, timeout = 30 } = options; let attempts = 0; const maxAttempts = wait ? Math.ceil(timeout) : 1; const delayMs = 1e3; while (attempts < maxAttempts) { try { const stampData = await this.makeRequest("GET", `/stamp/${stampId}`); if (!stampData.merkle_proof) { if (!wait || attempts === maxAttempts - 1) { throw new BasestampError("Merkle proof not yet available"); } await new Promise((resolve) => setTimeout(resolve, delayMs)); attempts++; continue; } return new Stamp(stampData, verifyMerkleProof, calculateSHA256); } catch (error) { if (error instanceof BasestampError) { throw error; } throw new BasestampError(`Failed to get stamp: ${error}`); } } throw new BasestampError(`Timeout waiting for stamp after ${timeout} seconds`); } /** * @deprecated Use getStamp() which now returns a Stamp object with verify method */ async getStampLegacy(stampId) { const response = await this.makeRequest("GET", `/stamp/${stampId}`); return response; } /** * @deprecated Use getStamp() and call stamp.getMerkleProof() instead */ async get_merkle_proof(stampId, wait = false, timeout = 30) { const stamp = await this.getStamp(stampId, { wait, timeout }); return stamp.getMerkleProof(); } /** * @deprecated Use getStamp() and call stamp.verify() instead */ async verifyStamp(stampId, hashValue) { try { const stamp = await this.getStamp(stampId); const hashToVerify = hashValue || stamp.original_hash; return stamp.verify(hashToVerify); } catch (error) { if (error instanceof BasestampError) { throw error; } throw new BasestampError(`Failed to verify stamp: ${error}`); } } async info() { const response = await this.makeRequest("GET", "/info"); return response; } async health() { const response = await this.makeRequest("GET", "/health"); return response; } async batchStats() { const response = await this.makeRequest("GET", "/batch/stats"); return response; } async makeRequest(method, path, body) { const url = `${this.baseURL}${path}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const init = { method, headers: { "Content-Type": "application/json" }, signal: controller.signal }; if (body && method === "POST") { init.body = JSON.stringify(body); } const response = await (0, import_cross_fetch.default)(url, init); clearTimeout(timeoutId); if (!response.ok) { throw new BasestampError(`Server returned error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { clearTimeout(timeoutId); if (error instanceof BasestampError) { throw error; } if (error.name === "AbortError") { throw new BasestampError("Request timeout"); } throw new BasestampError(`Request failed: ${error}`); } } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { BasestampClient, BasestampError, MerkleProof, Stamp, calculateSHA256, verifyMerkleProof });