@otskit/client 0.5.0 → 0.6.1

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/dist/index.js CHANGED
@@ -29,7 +29,8 @@ var OpenTimestampsClientError = class extends Error {
29
29
  constructor(message, options) {
30
30
  super(message);
31
31
  this.name = this.constructor.name;
32
- if (options?.cause !== void 0) this.cause = options.cause;
32
+ if (options?.cause !== void 0)
33
+ this.cause = options.cause;
33
34
  Error.captureStackTrace?.(this, this.constructor);
34
35
  }
35
36
  };
@@ -328,7 +329,8 @@ async function executeRequest(request, maxBytes) {
328
329
  if (error instanceof NetworkError) throw error;
329
330
  if (error instanceof Error) {
330
331
  if (error.name === "AbortError") throw new NetworkError("Request aborted", { cause: error });
331
- if (error.message.includes("timeout")) throw new NetworkError("Request timeout", { cause: error });
332
+ if (error.message.includes("timeout"))
333
+ throw new NetworkError("Request timeout", { cause: error });
332
334
  throw new NetworkError(`Network request failed: ${error.message}`, { cause: error });
333
335
  }
334
336
  throw new NetworkError("Unknown network error");
@@ -342,10 +344,14 @@ function createTimeoutController(timeoutMs, parentSignal) {
342
344
  parentSignal?.removeEventListener("abort", onParentAbort);
343
345
  controller.abort(parentSignal?.reason);
344
346
  };
345
- controller.signal.addEventListener("abort", () => {
346
- clearTimeout(timeout);
347
- parentSignal?.removeEventListener("abort", onParentAbort);
348
- }, { once: true });
347
+ controller.signal.addEventListener(
348
+ "abort",
349
+ () => {
350
+ clearTimeout(timeout);
351
+ parentSignal?.removeEventListener("abort", onParentAbort);
352
+ },
353
+ { once: true }
354
+ );
349
355
  if (parentSignal) {
350
356
  if (parentSignal.aborted) {
351
357
  clearTimeout(timeout);
@@ -437,15 +443,8 @@ var ResilientNetworkLayer = class {
437
443
  }
438
444
  };
439
445
 
440
- // src/core/orchestration.ts
441
- import {
442
- DetachedTimestampFile,
443
- OpSHA256,
444
- OpAppend,
445
- OpSHA1,
446
- OpRIPEMD160,
447
- makeMerkleTree
448
- } from "@otskit/core";
446
+ // src/core/stamp.ts
447
+ import { DetachedTimestampFile, OpSHA256, OpAppend, makeMerkleTree } from "@otskit/core";
449
448
 
450
449
  // src/network/calendar.ts
451
450
  import {
@@ -603,6 +602,146 @@ var UrlWhitelist = class {
603
602
  var DEFAULT_CALENDAR_WHITELIST = new UrlWhitelist([...TRUSTED_CALENDAR_WHITELIST_PATTERNS]);
604
603
  var DEFAULT_AGGREGATORS = [...DEFAULT_AGGREGATOR_URLS];
605
604
 
605
+ // src/utils/hex.ts
606
+ function hexToBytes(hex) {
607
+ const clean = hex.trim().toLowerCase();
608
+ if (clean.length % 2 !== 0 || !/^[0-9a-f]*$/.test(clean)) throw new Error("Invalid hex string");
609
+ const out = new Uint8Array(clean.length / 2);
610
+ for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
611
+ return out;
612
+ }
613
+ function bytesToHex2(bytes) {
614
+ let s = "";
615
+ for (const b of bytes) s += b.toString(16).padStart(2, "0");
616
+ return s;
617
+ }
618
+
619
+ // src/core/shared.ts
620
+ function validateHash(hash) {
621
+ if (typeof hash === "string") {
622
+ const hex = hash.trim().toLowerCase();
623
+ if (!/^[0-9a-f]{64}$/.test(hex)) {
624
+ throw new ValidationError("Hash must be a 64-character hex string (SHA-256)");
625
+ }
626
+ return hexToBytes(hex);
627
+ }
628
+ if (hash.length !== 32) {
629
+ throw new ValidationError("Hash must be exactly 32 bytes (SHA-256)");
630
+ }
631
+ return Uint8Array.from(hash);
632
+ }
633
+ function secureNonce(n) {
634
+ const bytes = new Uint8Array(n);
635
+ if (!globalThis.crypto?.getRandomValues) {
636
+ throw new Error("secure RNG unavailable: globalThis.crypto.getRandomValues is required");
637
+ }
638
+ globalThis.crypto.getRandomValues(bytes);
639
+ return bytes;
640
+ }
641
+
642
+ // src/core/stamp.ts
643
+ async function orchestrateStamp(hash, calendars, networkLayer, validateCalendarUrl, logger, signal, minimumSuccessfulSubmissions = 2) {
644
+ if (calendars.length === 0) {
645
+ throw new ValidationError("at least one calendar is required to stamp");
646
+ }
647
+ if (!Number.isInteger(minimumSuccessfulSubmissions) || minimumSuccessfulSubmissions < 1) {
648
+ throw new ValidationError("minimumSuccessfulSubmissions must be an integer >= 1");
649
+ }
650
+ if (minimumSuccessfulSubmissions > calendars.length) {
651
+ throw new ValidationError(
652
+ `minimumSuccessfulSubmissions (${minimumSuccessfulSubmissions}) cannot exceed the number of calendars (${calendars.length})`
653
+ );
654
+ }
655
+ await Promise.all(calendars.map((url) => validateCalendarUrl(url)));
656
+ const digest = validateHash(hash);
657
+ logger?.info(`Starting stamp for ${bytesToHex2(digest)}`);
658
+ const detached = DetachedTimestampFile.fromHash(new OpSHA256(), digest);
659
+ const nonceAppended = detached.timestamp.add(new OpAppend(secureNonce(16)));
660
+ const merkleRoot = nonceAppended.add(new OpSHA256());
661
+ const merkleTip = makeMerkleTree([merkleRoot]);
662
+ const results = await Promise.allSettled(
663
+ calendars.map(
664
+ (url) => new CalendarClient(url, networkLayer, logger).submit(merkleTip.getDigest(), signal)
665
+ )
666
+ );
667
+ const successful = [];
668
+ const failed = [];
669
+ results.forEach((r, i) => {
670
+ const calendar = calendars[i];
671
+ if (r.status === "fulfilled") {
672
+ merkleTip.merge(r.value);
673
+ successful.push({ calendar });
674
+ logger?.info(`Submitted to ${calendar}`);
675
+ } else {
676
+ const error = r.reason instanceof Error ? r.reason : new Error(String(r.reason));
677
+ failed.push({ calendar, error });
678
+ logger?.warn(`Failed to submit to ${calendar}: ${error.message}`);
679
+ }
680
+ });
681
+ if (successful.length < minimumSuccessfulSubmissions) {
682
+ throw new StampError(
683
+ `Insufficient successful submissions (${successful.length}/${minimumSuccessfulSubmissions} required)`,
684
+ successful,
685
+ failed
686
+ );
687
+ }
688
+ return detached.serializeToBytes();
689
+ }
690
+
691
+ // src/core/upgrade.ts
692
+ import { DetachedTimestampFile as DetachedTimestampFile2 } from "@otskit/core";
693
+ var bytesEqFast = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)) === 0;
694
+ async function orchestrateUpgrade(incompleteProof, _calendars, networkLayer, logger, signal) {
695
+ let detached;
696
+ try {
697
+ detached = DetachedTimestampFile2.deserialize(new Uint8Array(incompleteProof));
698
+ } catch (error) {
699
+ throw new ValidationError("Invalid .ots proof format", {
700
+ /* v8 ignore next */
701
+ ...error instanceof Error ? { cause: error } : {}
702
+ });
703
+ }
704
+ if (detached.timestamp.isTimestampComplete()) {
705
+ logger?.info("Proof already complete; nothing to upgrade");
706
+ return Buffer.from(incompleteProof);
707
+ }
708
+ const before = detached.serializeToBytes();
709
+ for (const subStamp of detached.timestamp.directlyVerified()) {
710
+ if (subStamp.isTimestampComplete()) continue;
711
+ for (const att of subStamp.attestations) {
712
+ if (att.kind !== "pending") continue;
713
+ if (!DEFAULT_CALENDAR_WHITELIST.contains(att.uri)) {
714
+ logger?.warn(`Ignoring attestation from non-whitelisted calendar ${att.uri}`);
715
+ continue;
716
+ }
717
+ try {
718
+ const upgraded = await new CalendarClient(att.uri, networkLayer, logger).getTimestamp(
719
+ subStamp.getDigest(),
720
+ signal
721
+ );
722
+ subStamp.merge(upgraded);
723
+ } catch (err) {
724
+ if (err instanceof CommitmentNotFoundError) {
725
+ logger?.debug(`Calendar ${att.uri} has not confirmed yet`);
726
+ continue;
727
+ }
728
+ logger?.warn(
729
+ `Failed to query ${att.uri}: ${err instanceof Error ? err.message : String(err)}`
730
+ );
731
+ }
732
+ }
733
+ }
734
+ const after = detached.serializeToBytes();
735
+ if (bytesEqFast(before, after)) {
736
+ throw new UpgradeError("No calendar has confirmed the timestamp yet (Bitcoin not yet mined)");
737
+ }
738
+ return Buffer.from(after);
739
+ }
740
+
741
+ // src/core/verify.ts
742
+ import { DetachedTimestampFile as DetachedTimestampFile3, OpSHA1, OpRIPEMD160 } from "@otskit/core";
743
+ import { timingSafeEqual } from "crypto";
744
+
606
745
  // src/network/esplora.ts
607
746
  import { createHash } from "crypto";
608
747
  import { verifyAgainstRawHeader, VerificationError } from "@otskit/core";
@@ -696,10 +835,18 @@ var EsploraClient = class {
696
835
  this.#logger?.debug(`Esplora raw header ${hash}`);
697
836
  const response = await this.#networkLayer.request(
698
837
  this.#url,
699
- { url: `${this.#url}/block/${hash}/header`, method: "GET", headers: { Accept: "application/octet-stream" } },
838
+ { url: `${this.#url}/block/${hash}/header`, method: "GET", headers: { Accept: "text/plain" } },
700
839
  signal
701
840
  );
702
- const data = response.data;
841
+ const hex = this.#decode(response.data).trim();
842
+ let data;
843
+ try {
844
+ data = hexToBytes(hex);
845
+ } catch (cause) {
846
+ throw new EsploraResponseError("esplora returned a non-hex raw block header", {
847
+ ...cause instanceof Error ? { cause } : {}
848
+ });
849
+ }
703
850
  if (data.length !== RAW_HEADER_SIZE) {
704
851
  throw new EsploraResponseError(
705
852
  `raw block header must be ${RAW_HEADER_SIZE} bytes; got ${data.length}`
@@ -738,8 +885,98 @@ async function verifyTimestampAttestation(digest, attestation, explorer, signal)
738
885
  return verifyAgainstRawHeader(digest, rawHeader);
739
886
  }
740
887
 
741
- // src/core/orchestration.ts
742
- import { timingSafeEqual } from "crypto";
888
+ // src/core/verify.ts
889
+ var MAX_BITCOIN_ATTESTATIONS = 10;
890
+ function timingSafeEq(a, b) {
891
+ if (a.length !== b.length) return false;
892
+ return timingSafeEqual(a, b);
893
+ }
894
+ async function orchestrateVerify(proof, networkLayer, originalDataHash, logger, signal, esploraUrl) {
895
+ let detached;
896
+ try {
897
+ detached = DetachedTimestampFile3.deserialize(new Uint8Array(proof));
898
+ } catch (cause) {
899
+ throw new ValidationError("Invalid .ots proof format", {
900
+ ...cause instanceof Error ? { cause } : {}
901
+ });
902
+ }
903
+ if (detached.fileHashOp instanceof OpSHA1 || detached.fileHashOp instanceof OpRIPEMD160) {
904
+ return {
905
+ status: "invalid",
906
+ reason: `This proof uses ${detached.fileHashOp.tagName} (a weak hash algorithm). Re-stamp the original file with SHA-256 to get a verifiable proof.`
907
+ };
908
+ }
909
+ if (originalDataHash !== void 0) {
910
+ let expected;
911
+ try {
912
+ expected = validateHash(originalDataHash);
913
+ } catch (err) {
914
+ throw new ValidationError(err instanceof Error ? err.message : "Invalid hash format", {
915
+ ...err instanceof Error ? { cause: err } : {}
916
+ });
917
+ }
918
+ if (!timingSafeEq(expected, detached.fileDigest())) {
919
+ return {
920
+ status: "invalid",
921
+ reason: "File hash does not match proof \u2014 file may have been modified"
922
+ };
923
+ }
924
+ }
925
+ const allBitcoin = detached.timestamp.allAttestations().filter(({ attestation }) => attestation.kind === "bitcoin");
926
+ const seenHeights = /* @__PURE__ */ new Set();
927
+ const deduped = allBitcoin.filter(({ attestation }) => {
928
+ if (attestation.kind !== "bitcoin") return false;
929
+ if (seenHeights.has(attestation.height)) {
930
+ logger?.debug(`Skipping duplicate Bitcoin attestation at height ${attestation.height}`);
931
+ return false;
932
+ }
933
+ seenHeights.add(attestation.height);
934
+ return true;
935
+ });
936
+ const bitcoinAtts = deduped.slice(0, MAX_BITCOIN_ATTESTATIONS);
937
+ if (deduped.length > MAX_BITCOIN_ATTESTATIONS) {
938
+ logger?.warn(
939
+ `Proof has ${deduped.length} unique Bitcoin attestations; verifying only the first ${MAX_BITCOIN_ATTESTATIONS}`
940
+ );
941
+ }
942
+ if (bitcoinAtts.length === 0) {
943
+ const hasLitecoin = detached.timestamp.allAttestations().some(({ attestation }) => attestation.kind === "litecoin");
944
+ return {
945
+ status: "pending",
946
+ reason: hasLitecoin ? "Litecoin-only attestation is not supported by this client" : "No Bitcoin attestation found \u2014 timestamp not yet confirmed"
947
+ };
948
+ }
949
+ const explorer = new EsploraClient(networkLayer, {
950
+ ...esploraUrl !== void 0 ? { url: esploraUrl } : {},
951
+ ...logger !== void 0 ? { logger } : {}
952
+ });
953
+ let lastNetworkError;
954
+ let lastCryptoError;
955
+ for (const { msg, attestation } of bitcoinAtts) {
956
+ if (attestation.kind !== "bitcoin") continue;
957
+ try {
958
+ const blockTime = await verifyTimestampAttestation(msg, attestation, explorer, signal);
959
+ logger?.info(`Verified against Bitcoin block ${attestation.height}`);
960
+ return { status: "verified", blockHeight: attestation.height, blockTime };
961
+ } catch (err) {
962
+ const message = err instanceof Error ? err.message : String(err);
963
+ if (err instanceof NetworkError || err instanceof EsploraResponseError) {
964
+ lastNetworkError = message;
965
+ logger?.warn(`Network error at block ${attestation.height}: ${message}`);
966
+ } else {
967
+ lastCryptoError = message;
968
+ logger?.warn(`Crypto verification failed at block ${attestation.height}: ${message}`);
969
+ }
970
+ }
971
+ }
972
+ if (lastCryptoError !== void 0) {
973
+ return { status: "invalid", reason: `Cryptographic verification failed: ${lastCryptoError}` };
974
+ }
975
+ return {
976
+ status: "network_error",
977
+ reason: `Could not reach Bitcoin blockchain: ${lastNetworkError ?? "unknown error"}`
978
+ };
979
+ }
743
980
 
744
981
  // src/security/ssrf.ts
745
982
  import { lookup } from "dns/promises";
@@ -758,7 +995,7 @@ var BLOCKED_CIDRS_V4 = [
758
995
  ];
759
996
  function ipv4ToUint32(ip) {
760
997
  const parts = ip.split(".");
761
- return (parseInt(parts[0], 10) << 24 | parseInt(parts[1], 10) << 16 | parseInt(parts[2], 10) << 8 | parseInt(parts[3], 10)) >>> 0;
998
+ return (Number.parseInt(parts[0], 10) << 24 | Number.parseInt(parts[1], 10) << 16 | Number.parseInt(parts[2], 10) << 8 | Number.parseInt(parts[3], 10)) >>> 0;
762
999
  }
763
1000
  function assertNotPrivateIPv4(ip, calendarUrl) {
764
1001
  const n = ipv4ToUint32(ip);
@@ -837,215 +1074,6 @@ async function assertSafeCalendarUrl(url, options) {
837
1074
  }
838
1075
  }
839
1076
 
840
- // src/core/orchestration.ts
841
- var MAX_BITCOIN_ATTESTATIONS = 10;
842
- function validateHash(hash) {
843
- if (typeof hash === "string") {
844
- const hex = hash.trim().toLowerCase();
845
- if (!/^[0-9a-f]{64}$/.test(hex)) {
846
- throw new ValidationError("Hash must be a 64-character hex string (SHA-256)");
847
- }
848
- return Uint8Array.from(Buffer.from(hex, "hex"));
849
- }
850
- if (hash.length !== 32) {
851
- throw new ValidationError("Hash must be exactly 32 bytes (SHA-256)");
852
- }
853
- return Uint8Array.from(hash);
854
- }
855
- function secureNonce(n) {
856
- const bytes = new Uint8Array(n);
857
- if (!globalThis.crypto?.getRandomValues) {
858
- throw new Error("secure RNG unavailable: globalThis.crypto.getRandomValues is required");
859
- }
860
- globalThis.crypto.getRandomValues(bytes);
861
- return bytes;
862
- }
863
- function timingSafeEq(a, b) {
864
- if (a.length !== b.length) return false;
865
- return timingSafeEqual(a, b);
866
- }
867
- var bytesEqFast = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)) === 0;
868
- async function orchestrateStamp(hash, calendars, networkLayer, logger, signal, minimumSuccessfulSubmissions = 2, allowPrivateCalendars = false) {
869
- if (calendars.length === 0) {
870
- throw new ValidationError("at least one calendar is required to stamp");
871
- }
872
- if (!Number.isInteger(minimumSuccessfulSubmissions) || minimumSuccessfulSubmissions < 1) {
873
- throw new ValidationError("minimumSuccessfulSubmissions must be an integer >= 1");
874
- }
875
- if (minimumSuccessfulSubmissions > calendars.length) {
876
- throw new ValidationError(
877
- `minimumSuccessfulSubmissions (${minimumSuccessfulSubmissions}) cannot exceed the number of calendars (${calendars.length})`
878
- );
879
- }
880
- await Promise.all(
881
- calendars.map((url) => assertSafeCalendarUrl(url, { allowPrivate: allowPrivateCalendars }))
882
- );
883
- const digest = validateHash(hash);
884
- logger?.info(`Starting stamp for ${Buffer.from(digest).toString("hex")}`);
885
- const detached = DetachedTimestampFile.fromHash(new OpSHA256(), digest);
886
- const nonceAppended = detached.timestamp.add(new OpAppend(secureNonce(16)));
887
- const merkleRoot = nonceAppended.add(new OpSHA256());
888
- const merkleTip = makeMerkleTree([merkleRoot]);
889
- const results = await Promise.allSettled(
890
- calendars.map((url) => new CalendarClient(url, networkLayer, logger).submit(merkleTip.getDigest(), signal))
891
- );
892
- const successful = [];
893
- const failed = [];
894
- results.forEach((r, i) => {
895
- const calendar = calendars[i];
896
- if (r.status === "fulfilled") {
897
- merkleTip.merge(r.value);
898
- successful.push({ calendar });
899
- logger?.info(`Submitted to ${calendar}`);
900
- } else {
901
- const error = r.reason instanceof Error ? r.reason : new Error(String(r.reason));
902
- failed.push({ calendar, error });
903
- logger?.warn(`Failed to submit to ${calendar}: ${error.message}`);
904
- }
905
- });
906
- if (successful.length < minimumSuccessfulSubmissions) {
907
- throw new StampError(
908
- `Insufficient successful submissions (${successful.length}/${minimumSuccessfulSubmissions} required)`,
909
- successful,
910
- failed
911
- );
912
- }
913
- return Buffer.from(detached.serializeToBytes());
914
- }
915
- async function orchestrateUpgrade(incompleteProof, _calendars, networkLayer, logger, signal) {
916
- let detached;
917
- try {
918
- detached = DetachedTimestampFile.deserialize(new Uint8Array(incompleteProof));
919
- } catch (error) {
920
- throw new ValidationError("Invalid .ots proof format", {
921
- /* v8 ignore next */
922
- ...error instanceof Error ? { cause: error } : {}
923
- });
924
- }
925
- if (detached.timestamp.isTimestampComplete()) {
926
- logger?.info("Proof already complete; nothing to upgrade");
927
- return Buffer.from(incompleteProof);
928
- }
929
- const before = detached.serializeToBytes();
930
- for (const subStamp of detached.timestamp.directlyVerified()) {
931
- if (subStamp.isTimestampComplete()) continue;
932
- for (const att of subStamp.attestations) {
933
- if (att.kind !== "pending") continue;
934
- if (!DEFAULT_CALENDAR_WHITELIST.contains(att.uri)) {
935
- logger?.warn(`Ignoring attestation from non-whitelisted calendar ${att.uri}`);
936
- continue;
937
- }
938
- try {
939
- const upgraded = await new CalendarClient(att.uri, networkLayer, logger).getTimestamp(
940
- subStamp.getDigest(),
941
- signal
942
- );
943
- subStamp.merge(upgraded);
944
- } catch (err) {
945
- if (err instanceof CommitmentNotFoundError) {
946
- logger?.debug(`Calendar ${att.uri} has not confirmed yet`);
947
- continue;
948
- }
949
- logger?.warn(`Failed to query ${att.uri}: ${err instanceof Error ? err.message : String(err)}`);
950
- }
951
- }
952
- }
953
- const after = detached.serializeToBytes();
954
- if (bytesEqFast(before, after)) {
955
- throw new UpgradeError("No calendar has confirmed the timestamp yet (Bitcoin not yet mined)");
956
- }
957
- return Buffer.from(after);
958
- }
959
- async function orchestrateVerify(proof, networkLayer, originalDataHash, logger, signal, esploraUrl) {
960
- let detached;
961
- try {
962
- detached = DetachedTimestampFile.deserialize(new Uint8Array(proof));
963
- } catch (cause) {
964
- throw new ValidationError("Invalid .ots proof format", {
965
- ...cause instanceof Error ? { cause } : {}
966
- });
967
- }
968
- if (detached.fileHashOp instanceof OpSHA1 || detached.fileHashOp instanceof OpRIPEMD160) {
969
- return {
970
- status: "invalid",
971
- reason: `This proof uses ${detached.fileHashOp.tagName} (a weak hash algorithm). Re-stamp the original file with SHA-256 to get a verifiable proof.`
972
- };
973
- }
974
- if (originalDataHash !== void 0) {
975
- let expected;
976
- try {
977
- expected = validateHash(originalDataHash);
978
- } catch (err) {
979
- throw new ValidationError(
980
- err instanceof Error ? err.message : "Invalid hash format",
981
- { ...err instanceof Error ? { cause: err } : {} }
982
- );
983
- }
984
- if (!timingSafeEq(expected, detached.fileDigest())) {
985
- return { status: "invalid", reason: "File hash does not match proof \u2014 file may have been modified" };
986
- }
987
- }
988
- const allBitcoin = detached.timestamp.allAttestations().filter(({ attestation }) => attestation.kind === "bitcoin");
989
- const seenHeights = /* @__PURE__ */ new Set();
990
- const deduped = allBitcoin.filter(({ attestation }) => {
991
- if (attestation.kind !== "bitcoin") return false;
992
- if (seenHeights.has(attestation.height)) {
993
- logger?.debug(`Skipping duplicate Bitcoin attestation at height ${attestation.height}`);
994
- return false;
995
- }
996
- seenHeights.add(attestation.height);
997
- return true;
998
- });
999
- const bitcoinAtts = deduped.slice(0, MAX_BITCOIN_ATTESTATIONS);
1000
- if (deduped.length > MAX_BITCOIN_ATTESTATIONS) {
1001
- logger?.warn(
1002
- `Proof has ${deduped.length} unique Bitcoin attestations; verifying only the first ${MAX_BITCOIN_ATTESTATIONS}`
1003
- );
1004
- }
1005
- if (bitcoinAtts.length === 0) {
1006
- const hasLitecoin = detached.timestamp.allAttestations().some(({ attestation }) => attestation.kind === "litecoin");
1007
- return {
1008
- status: "pending",
1009
- reason: hasLitecoin ? "Litecoin-only attestation is not supported by this client" : "No Bitcoin attestation found \u2014 timestamp not yet confirmed"
1010
- };
1011
- }
1012
- const explorer = new EsploraClient(networkLayer, {
1013
- ...esploraUrl !== void 0 ? { url: esploraUrl } : {},
1014
- ...logger !== void 0 ? { logger } : {}
1015
- });
1016
- let lastNetworkError;
1017
- let lastCryptoError;
1018
- for (const { msg, attestation } of bitcoinAtts) {
1019
- if (attestation.kind !== "bitcoin") continue;
1020
- try {
1021
- const blockTime = await verifyTimestampAttestation(
1022
- Uint8Array.from(msg).reverse(),
1023
- attestation,
1024
- explorer,
1025
- signal
1026
- );
1027
- logger?.info(`Verified against Bitcoin block ${attestation.height}`);
1028
- return { status: "verified", blockHeight: attestation.height, blockTime };
1029
- } catch (err) {
1030
- const message = err instanceof Error ? err.message : String(err);
1031
- if (err instanceof NetworkError || err instanceof EsploraResponseError) {
1032
- lastNetworkError = message;
1033
- logger?.warn(`Network error at block ${attestation.height}: ${message}`);
1034
- } else {
1035
- lastCryptoError = message;
1036
- logger?.warn(`Crypto verification failed at block ${attestation.height}: ${message}`);
1037
- }
1038
- }
1039
- }
1040
- if (lastCryptoError !== void 0) {
1041
- return { status: "invalid", reason: `Cryptographic verification failed: ${lastCryptoError}` };
1042
- }
1043
- return {
1044
- status: "network_error",
1045
- reason: `Could not reach Bitcoin blockchain: ${lastNetworkError ?? "unknown error"}`
1046
- };
1047
- }
1048
-
1049
1077
  // src/client.ts
1050
1078
  var OpenTimestampsClient = class {
1051
1079
  calendars;
@@ -1057,7 +1085,7 @@ var OpenTimestampsClient = class {
1057
1085
  esploraUrl;
1058
1086
  /**
1059
1087
  * Create a new OpenTimestamps client
1060
- *
1088
+ *
1061
1089
  * @param options Client configuration options
1062
1090
  */
1063
1091
  constructor(options = {}) {
@@ -1103,15 +1131,15 @@ var OpenTimestampsClient = class {
1103
1131
  }
1104
1132
  /**
1105
1133
  * Create a timestamp by submitting a hash to calendar servers
1106
- *
1134
+ *
1107
1135
  * @param hash SHA-256 hash of the data to timestamp (as Buffer or hex string)
1108
1136
  * @param options Operation-specific options
1109
1137
  * @returns Initial .ots proof with pending attestations
1110
- *
1138
+ *
1111
1139
  * @throws {ValidationError} If the hash is invalid
1112
1140
  * @throws {StampError} If submission fails to all calendars
1113
1141
  * @throws {NetworkError} If network errors occur
1114
- *
1142
+ *
1115
1143
  * @example
1116
1144
  * ```typescript
1117
1145
  * const hash = crypto.createHash('sha256').update('my data').digest()
@@ -1121,34 +1149,36 @@ var OpenTimestampsClient = class {
1121
1149
  */
1122
1150
  async stamp(hash, options) {
1123
1151
  const signal = options?.signal || this.globalSignal;
1124
- return orchestrateStamp(
1152
+ const allowPrivate = this.allowPrivateCalendars;
1153
+ const bytes = await orchestrateStamp(
1125
1154
  hash,
1126
1155
  this.calendars,
1127
1156
  this.networkLayer,
1157
+ (url) => assertSafeCalendarUrl(url, { allowPrivate }),
1128
1158
  this.logger,
1129
1159
  signal,
1130
- this.minimumSuccessfulSubmissions,
1131
- this.allowPrivateCalendars
1160
+ this.minimumSuccessfulSubmissions
1132
1161
  );
1162
+ return Buffer.from(bytes);
1133
1163
  }
1134
1164
  /**
1135
1165
  * Upgrade an incomplete timestamp proof by querying calendars for Bitcoin confirmation
1136
- *
1166
+ *
1137
1167
  * @param incompleteProof The initial .ots proof returned by stamp()
1138
1168
  * @param options Operation-specific options
1139
1169
  * @returns Upgraded .ots proof with Bitcoin attestation (if available)
1140
- *
1170
+ *
1141
1171
  * @throws {ValidationError} If the proof format is invalid
1142
1172
  * @throws {UpgradeError} If no calendar has confirmed the timestamp yet
1143
1173
  * @throws {NetworkError} If network errors occur
1144
- *
1174
+ *
1145
1175
  * @example
1146
1176
  * ```typescript
1147
1177
  * // Proof already has pending attestations from stamp()
1148
1178
  * const upgradedProof = await client.upgrade(incompleteProof)
1149
- *
1179
+ *
1150
1180
  * // If upgrade throws UpgradeError, Bitcoin hasn't confirmed yet
1151
- * // Retry later (typically 10-60 minutes after stamp)
1181
+ * // Retry later (typically ~60 minutes after stamp)
1152
1182
  * ```
1153
1183
  */
1154
1184
  async upgrade(incompleteProof, options) {
@@ -1163,15 +1193,15 @@ var OpenTimestampsClient = class {
1163
1193
  }
1164
1194
  /**
1165
1195
  * Verify a complete timestamp proof against the Bitcoin blockchain
1166
- *
1196
+ *
1167
1197
  * @param proof The complete .ots proof with Bitcoin attestation
1168
1198
  * @param originalDataHash Optional: the original data hash to verify against
1169
1199
  * @returns Verification result with block details
1170
- *
1200
+ *
1171
1201
  * @example
1172
1202
  * ```typescript
1173
1203
  * const result = await client.verify(completeProof, originalHash)
1174
- *
1204
+ *
1175
1205
  * if (result.valid) {
1176
1206
  * console.log(`Timestamp confirmed in Bitcoin block ${result.blockHeight}`)
1177
1207
  * console.log(`Block timestamp: ${new Date(result.timestamp! * 1000)}`)
@@ -1181,12 +1211,19 @@ var OpenTimestampsClient = class {
1181
1211
  * ```
1182
1212
  */
1183
1213
  async verify(proof, originalDataHash) {
1184
- return orchestrateVerify(proof, this.networkLayer, originalDataHash, this.logger, this.globalSignal, this.esploraUrl);
1214
+ return orchestrateVerify(
1215
+ proof,
1216
+ this.networkLayer,
1217
+ originalDataHash,
1218
+ this.logger,
1219
+ this.globalSignal,
1220
+ this.esploraUrl
1221
+ );
1185
1222
  }
1186
1223
  /**
1187
1224
  * Get the current state of the circuit breaker for a calendar
1188
1225
  * Useful for monitoring and debugging
1189
- *
1226
+ *
1190
1227
  * @param calendarUrl The calendar URL to check
1191
1228
  * @returns Circuit state: 'CLOSED', 'OPEN', or 'HALF_OPEN' (undefined if not yet initialized)
1192
1229
  */
@@ -1196,7 +1233,7 @@ var OpenTimestampsClient = class {
1196
1233
  /**
1197
1234
  * Reset the circuit breaker for a specific calendar
1198
1235
  * Use this to manually recover a calendar that has been marked as failing
1199
- *
1236
+ *
1200
1237
  * @param calendarUrl The calendar URL to reset
1201
1238
  */
1202
1239
  resetCircuit(calendarUrl) {
@@ -1214,7 +1251,7 @@ var OpenTimestampsClient = class {
1214
1251
  };
1215
1252
 
1216
1253
  // src/index.ts
1217
- import { DetachedTimestampFile as DetachedTimestampFile2, Timestamp as Timestamp2 } from "@otskit/core";
1254
+ import { DetachedTimestampFile as DetachedTimestampFile4, Timestamp as Timestamp2 } from "@otskit/core";
1218
1255
  import { verifyAgainstBlockheader } from "@otskit/core";
1219
1256
 
1220
1257
  // src/utils/hash.ts
@@ -1239,7 +1276,7 @@ export {
1239
1276
  DEFAULT_CALENDARS,
1240
1277
  DEFAULT_CALENDAR_WHITELIST,
1241
1278
  DEFAULT_RESILIENCE,
1242
- DetachedTimestampFile2 as DetachedTimestampFile,
1279
+ DetachedTimestampFile4 as DetachedTimestampFile,
1243
1280
  EsploraClient,
1244
1281
  EsploraResponseError,
1245
1282
  MAX_CALENDAR_RESPONSE_SIZE,