@otskit/client 0.5.0 → 0.6.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/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";
@@ -738,8 +877,103 @@ async function verifyTimestampAttestation(digest, attestation, explorer, signal)
738
877
  return verifyAgainstRawHeader(digest, rawHeader);
739
878
  }
740
879
 
741
- // src/core/orchestration.ts
742
- import { timingSafeEqual } from "crypto";
880
+ // src/core/verify.ts
881
+ var MAX_BITCOIN_ATTESTATIONS = 10;
882
+ function timingSafeEq(a, b) {
883
+ if (a.length !== b.length) return false;
884
+ return timingSafeEqual(a, b);
885
+ }
886
+ async function orchestrateVerify(proof, networkLayer, originalDataHash, logger, signal, esploraUrl) {
887
+ let detached;
888
+ try {
889
+ detached = DetachedTimestampFile3.deserialize(new Uint8Array(proof));
890
+ } catch (cause) {
891
+ throw new ValidationError("Invalid .ots proof format", {
892
+ ...cause instanceof Error ? { cause } : {}
893
+ });
894
+ }
895
+ if (detached.fileHashOp instanceof OpSHA1 || detached.fileHashOp instanceof OpRIPEMD160) {
896
+ return {
897
+ status: "invalid",
898
+ reason: `This proof uses ${detached.fileHashOp.tagName} (a weak hash algorithm). Re-stamp the original file with SHA-256 to get a verifiable proof.`
899
+ };
900
+ }
901
+ if (originalDataHash !== void 0) {
902
+ let expected;
903
+ try {
904
+ expected = validateHash(originalDataHash);
905
+ } catch (err) {
906
+ throw new ValidationError(err instanceof Error ? err.message : "Invalid hash format", {
907
+ ...err instanceof Error ? { cause: err } : {}
908
+ });
909
+ }
910
+ if (!timingSafeEq(expected, detached.fileDigest())) {
911
+ return {
912
+ status: "invalid",
913
+ reason: "File hash does not match proof \u2014 file may have been modified"
914
+ };
915
+ }
916
+ }
917
+ const allBitcoin = detached.timestamp.allAttestations().filter(({ attestation }) => attestation.kind === "bitcoin");
918
+ const seenHeights = /* @__PURE__ */ new Set();
919
+ const deduped = allBitcoin.filter(({ attestation }) => {
920
+ if (attestation.kind !== "bitcoin") return false;
921
+ if (seenHeights.has(attestation.height)) {
922
+ logger?.debug(`Skipping duplicate Bitcoin attestation at height ${attestation.height}`);
923
+ return false;
924
+ }
925
+ seenHeights.add(attestation.height);
926
+ return true;
927
+ });
928
+ const bitcoinAtts = deduped.slice(0, MAX_BITCOIN_ATTESTATIONS);
929
+ if (deduped.length > MAX_BITCOIN_ATTESTATIONS) {
930
+ logger?.warn(
931
+ `Proof has ${deduped.length} unique Bitcoin attestations; verifying only the first ${MAX_BITCOIN_ATTESTATIONS}`
932
+ );
933
+ }
934
+ if (bitcoinAtts.length === 0) {
935
+ const hasLitecoin = detached.timestamp.allAttestations().some(({ attestation }) => attestation.kind === "litecoin");
936
+ return {
937
+ status: "pending",
938
+ reason: hasLitecoin ? "Litecoin-only attestation is not supported by this client" : "No Bitcoin attestation found \u2014 timestamp not yet confirmed"
939
+ };
940
+ }
941
+ const explorer = new EsploraClient(networkLayer, {
942
+ ...esploraUrl !== void 0 ? { url: esploraUrl } : {},
943
+ ...logger !== void 0 ? { logger } : {}
944
+ });
945
+ let lastNetworkError;
946
+ let lastCryptoError;
947
+ for (const { msg, attestation } of bitcoinAtts) {
948
+ if (attestation.kind !== "bitcoin") continue;
949
+ try {
950
+ const blockTime = await verifyTimestampAttestation(
951
+ Uint8Array.from(msg).reverse(),
952
+ attestation,
953
+ explorer,
954
+ signal
955
+ );
956
+ logger?.info(`Verified against Bitcoin block ${attestation.height}`);
957
+ return { status: "verified", blockHeight: attestation.height, blockTime };
958
+ } catch (err) {
959
+ const message = err instanceof Error ? err.message : String(err);
960
+ if (err instanceof NetworkError || err instanceof EsploraResponseError) {
961
+ lastNetworkError = message;
962
+ logger?.warn(`Network error at block ${attestation.height}: ${message}`);
963
+ } else {
964
+ lastCryptoError = message;
965
+ logger?.warn(`Crypto verification failed at block ${attestation.height}: ${message}`);
966
+ }
967
+ }
968
+ }
969
+ if (lastCryptoError !== void 0) {
970
+ return { status: "invalid", reason: `Cryptographic verification failed: ${lastCryptoError}` };
971
+ }
972
+ return {
973
+ status: "network_error",
974
+ reason: `Could not reach Bitcoin blockchain: ${lastNetworkError ?? "unknown error"}`
975
+ };
976
+ }
743
977
 
744
978
  // src/security/ssrf.ts
745
979
  import { lookup } from "dns/promises";
@@ -758,7 +992,7 @@ var BLOCKED_CIDRS_V4 = [
758
992
  ];
759
993
  function ipv4ToUint32(ip) {
760
994
  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;
995
+ 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
996
  }
763
997
  function assertNotPrivateIPv4(ip, calendarUrl) {
764
998
  const n = ipv4ToUint32(ip);
@@ -837,215 +1071,6 @@ async function assertSafeCalendarUrl(url, options) {
837
1071
  }
838
1072
  }
839
1073
 
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
1074
  // src/client.ts
1050
1075
  var OpenTimestampsClient = class {
1051
1076
  calendars;
@@ -1057,7 +1082,7 @@ var OpenTimestampsClient = class {
1057
1082
  esploraUrl;
1058
1083
  /**
1059
1084
  * Create a new OpenTimestamps client
1060
- *
1085
+ *
1061
1086
  * @param options Client configuration options
1062
1087
  */
1063
1088
  constructor(options = {}) {
@@ -1103,15 +1128,15 @@ var OpenTimestampsClient = class {
1103
1128
  }
1104
1129
  /**
1105
1130
  * Create a timestamp by submitting a hash to calendar servers
1106
- *
1131
+ *
1107
1132
  * @param hash SHA-256 hash of the data to timestamp (as Buffer or hex string)
1108
1133
  * @param options Operation-specific options
1109
1134
  * @returns Initial .ots proof with pending attestations
1110
- *
1135
+ *
1111
1136
  * @throws {ValidationError} If the hash is invalid
1112
1137
  * @throws {StampError} If submission fails to all calendars
1113
1138
  * @throws {NetworkError} If network errors occur
1114
- *
1139
+ *
1115
1140
  * @example
1116
1141
  * ```typescript
1117
1142
  * const hash = crypto.createHash('sha256').update('my data').digest()
@@ -1121,34 +1146,36 @@ var OpenTimestampsClient = class {
1121
1146
  */
1122
1147
  async stamp(hash, options) {
1123
1148
  const signal = options?.signal || this.globalSignal;
1124
- return orchestrateStamp(
1149
+ const allowPrivate = this.allowPrivateCalendars;
1150
+ const bytes = await orchestrateStamp(
1125
1151
  hash,
1126
1152
  this.calendars,
1127
1153
  this.networkLayer,
1154
+ (url) => assertSafeCalendarUrl(url, { allowPrivate }),
1128
1155
  this.logger,
1129
1156
  signal,
1130
- this.minimumSuccessfulSubmissions,
1131
- this.allowPrivateCalendars
1157
+ this.minimumSuccessfulSubmissions
1132
1158
  );
1159
+ return Buffer.from(bytes);
1133
1160
  }
1134
1161
  /**
1135
1162
  * Upgrade an incomplete timestamp proof by querying calendars for Bitcoin confirmation
1136
- *
1163
+ *
1137
1164
  * @param incompleteProof The initial .ots proof returned by stamp()
1138
1165
  * @param options Operation-specific options
1139
1166
  * @returns Upgraded .ots proof with Bitcoin attestation (if available)
1140
- *
1167
+ *
1141
1168
  * @throws {ValidationError} If the proof format is invalid
1142
1169
  * @throws {UpgradeError} If no calendar has confirmed the timestamp yet
1143
1170
  * @throws {NetworkError} If network errors occur
1144
- *
1171
+ *
1145
1172
  * @example
1146
1173
  * ```typescript
1147
1174
  * // Proof already has pending attestations from stamp()
1148
1175
  * const upgradedProof = await client.upgrade(incompleteProof)
1149
- *
1176
+ *
1150
1177
  * // If upgrade throws UpgradeError, Bitcoin hasn't confirmed yet
1151
- * // Retry later (typically 10-60 minutes after stamp)
1178
+ * // Retry later (typically ~60 minutes after stamp)
1152
1179
  * ```
1153
1180
  */
1154
1181
  async upgrade(incompleteProof, options) {
@@ -1163,15 +1190,15 @@ var OpenTimestampsClient = class {
1163
1190
  }
1164
1191
  /**
1165
1192
  * Verify a complete timestamp proof against the Bitcoin blockchain
1166
- *
1193
+ *
1167
1194
  * @param proof The complete .ots proof with Bitcoin attestation
1168
1195
  * @param originalDataHash Optional: the original data hash to verify against
1169
1196
  * @returns Verification result with block details
1170
- *
1197
+ *
1171
1198
  * @example
1172
1199
  * ```typescript
1173
1200
  * const result = await client.verify(completeProof, originalHash)
1174
- *
1201
+ *
1175
1202
  * if (result.valid) {
1176
1203
  * console.log(`Timestamp confirmed in Bitcoin block ${result.blockHeight}`)
1177
1204
  * console.log(`Block timestamp: ${new Date(result.timestamp! * 1000)}`)
@@ -1181,12 +1208,19 @@ var OpenTimestampsClient = class {
1181
1208
  * ```
1182
1209
  */
1183
1210
  async verify(proof, originalDataHash) {
1184
- return orchestrateVerify(proof, this.networkLayer, originalDataHash, this.logger, this.globalSignal, this.esploraUrl);
1211
+ return orchestrateVerify(
1212
+ proof,
1213
+ this.networkLayer,
1214
+ originalDataHash,
1215
+ this.logger,
1216
+ this.globalSignal,
1217
+ this.esploraUrl
1218
+ );
1185
1219
  }
1186
1220
  /**
1187
1221
  * Get the current state of the circuit breaker for a calendar
1188
1222
  * Useful for monitoring and debugging
1189
- *
1223
+ *
1190
1224
  * @param calendarUrl The calendar URL to check
1191
1225
  * @returns Circuit state: 'CLOSED', 'OPEN', or 'HALF_OPEN' (undefined if not yet initialized)
1192
1226
  */
@@ -1196,7 +1230,7 @@ var OpenTimestampsClient = class {
1196
1230
  /**
1197
1231
  * Reset the circuit breaker for a specific calendar
1198
1232
  * Use this to manually recover a calendar that has been marked as failing
1199
- *
1233
+ *
1200
1234
  * @param calendarUrl The calendar URL to reset
1201
1235
  */
1202
1236
  resetCircuit(calendarUrl) {
@@ -1214,7 +1248,7 @@ var OpenTimestampsClient = class {
1214
1248
  };
1215
1249
 
1216
1250
  // src/index.ts
1217
- import { DetachedTimestampFile as DetachedTimestampFile2, Timestamp as Timestamp2 } from "@otskit/core";
1251
+ import { DetachedTimestampFile as DetachedTimestampFile4, Timestamp as Timestamp2 } from "@otskit/core";
1218
1252
  import { verifyAgainstBlockheader } from "@otskit/core";
1219
1253
 
1220
1254
  // src/utils/hash.ts
@@ -1239,7 +1273,7 @@ export {
1239
1273
  DEFAULT_CALENDARS,
1240
1274
  DEFAULT_CALENDAR_WHITELIST,
1241
1275
  DEFAULT_RESILIENCE,
1242
- DetachedTimestampFile2 as DetachedTimestampFile,
1276
+ DetachedTimestampFile4 as DetachedTimestampFile,
1243
1277
  EsploraClient,
1244
1278
  EsploraResponseError,
1245
1279
  MAX_CALENDAR_RESPONSE_SIZE,