@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.cjs CHANGED
@@ -29,7 +29,7 @@ __export(index_exports, {
29
29
  DEFAULT_CALENDARS: () => DEFAULT_CALENDARS,
30
30
  DEFAULT_CALENDAR_WHITELIST: () => DEFAULT_CALENDAR_WHITELIST,
31
31
  DEFAULT_RESILIENCE: () => DEFAULT_RESILIENCE,
32
- DetachedTimestampFile: () => import_core5.DetachedTimestampFile,
32
+ DetachedTimestampFile: () => import_core7.DetachedTimestampFile,
33
33
  EsploraClient: () => EsploraClient,
34
34
  EsploraResponseError: () => EsploraResponseError,
35
35
  MAX_CALENDAR_RESPONSE_SIZE: () => MAX_CALENDAR_RESPONSE_SIZE,
@@ -41,7 +41,7 @@ __export(index_exports, {
41
41
  ResilientNetworkLayer: () => ResilientNetworkLayer,
42
42
  SizeLimitExceededError: () => SizeLimitExceededError,
43
43
  StampError: () => StampError,
44
- Timestamp: () => import_core5.Timestamp,
44
+ Timestamp: () => import_core7.Timestamp,
45
45
  UpgradeError: () => UpgradeError,
46
46
  UrlWhitelist: () => UrlWhitelist,
47
47
  ValidationError: () => ValidationError,
@@ -49,7 +49,7 @@ __export(index_exports, {
49
49
  hashBuffer: () => hashBuffer,
50
50
  hashFile: () => hashFile,
51
51
  isVerified: () => isVerified,
52
- verifyAgainstBlockheader: () => import_core6.verifyAgainstBlockheader,
52
+ verifyAgainstBlockheader: () => import_core8.verifyAgainstBlockheader,
53
53
  verifyTimestampAttestation: () => verifyTimestampAttestation
54
54
  });
55
55
  module.exports = __toCommonJS(index_exports);
@@ -85,7 +85,8 @@ var OpenTimestampsClientError = class extends Error {
85
85
  constructor(message, options) {
86
86
  super(message);
87
87
  this.name = this.constructor.name;
88
- if (options?.cause !== void 0) this.cause = options.cause;
88
+ if (options?.cause !== void 0)
89
+ this.cause = options.cause;
89
90
  Error.captureStackTrace?.(this, this.constructor);
90
91
  }
91
92
  };
@@ -384,7 +385,8 @@ async function executeRequest(request, maxBytes) {
384
385
  if (error instanceof NetworkError) throw error;
385
386
  if (error instanceof Error) {
386
387
  if (error.name === "AbortError") throw new NetworkError("Request aborted", { cause: error });
387
- if (error.message.includes("timeout")) throw new NetworkError("Request timeout", { cause: error });
388
+ if (error.message.includes("timeout"))
389
+ throw new NetworkError("Request timeout", { cause: error });
388
390
  throw new NetworkError(`Network request failed: ${error.message}`, { cause: error });
389
391
  }
390
392
  throw new NetworkError("Unknown network error");
@@ -398,10 +400,14 @@ function createTimeoutController(timeoutMs, parentSignal) {
398
400
  parentSignal?.removeEventListener("abort", onParentAbort);
399
401
  controller.abort(parentSignal?.reason);
400
402
  };
401
- controller.signal.addEventListener("abort", () => {
402
- clearTimeout(timeout);
403
- parentSignal?.removeEventListener("abort", onParentAbort);
404
- }, { once: true });
403
+ controller.signal.addEventListener(
404
+ "abort",
405
+ () => {
406
+ clearTimeout(timeout);
407
+ parentSignal?.removeEventListener("abort", onParentAbort);
408
+ },
409
+ { once: true }
410
+ );
405
411
  if (parentSignal) {
406
412
  if (parentSignal.aborted) {
407
413
  clearTimeout(timeout);
@@ -493,8 +499,8 @@ var ResilientNetworkLayer = class {
493
499
  }
494
500
  };
495
501
 
496
- // src/core/orchestration.ts
497
- var import_core4 = require("@otskit/core");
502
+ // src/core/stamp.ts
503
+ var import_core3 = require("@otskit/core");
498
504
 
499
505
  // src/network/calendar.ts
500
506
  var import_core2 = require("@otskit/core");
@@ -646,9 +652,149 @@ var UrlWhitelist = class {
646
652
  var DEFAULT_CALENDAR_WHITELIST = new UrlWhitelist([...import_core2.TRUSTED_CALENDAR_WHITELIST_PATTERNS]);
647
653
  var DEFAULT_AGGREGATORS = [...import_core2.DEFAULT_AGGREGATOR_URLS];
648
654
 
655
+ // src/utils/hex.ts
656
+ function hexToBytes(hex) {
657
+ const clean = hex.trim().toLowerCase();
658
+ if (clean.length % 2 !== 0 || !/^[0-9a-f]*$/.test(clean)) throw new Error("Invalid hex string");
659
+ const out = new Uint8Array(clean.length / 2);
660
+ for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
661
+ return out;
662
+ }
663
+ function bytesToHex2(bytes) {
664
+ let s = "";
665
+ for (const b of bytes) s += b.toString(16).padStart(2, "0");
666
+ return s;
667
+ }
668
+
669
+ // src/core/shared.ts
670
+ function validateHash(hash) {
671
+ if (typeof hash === "string") {
672
+ const hex = hash.trim().toLowerCase();
673
+ if (!/^[0-9a-f]{64}$/.test(hex)) {
674
+ throw new ValidationError("Hash must be a 64-character hex string (SHA-256)");
675
+ }
676
+ return hexToBytes(hex);
677
+ }
678
+ if (hash.length !== 32) {
679
+ throw new ValidationError("Hash must be exactly 32 bytes (SHA-256)");
680
+ }
681
+ return Uint8Array.from(hash);
682
+ }
683
+ function secureNonce(n) {
684
+ const bytes = new Uint8Array(n);
685
+ if (!globalThis.crypto?.getRandomValues) {
686
+ throw new Error("secure RNG unavailable: globalThis.crypto.getRandomValues is required");
687
+ }
688
+ globalThis.crypto.getRandomValues(bytes);
689
+ return bytes;
690
+ }
691
+
692
+ // src/core/stamp.ts
693
+ async function orchestrateStamp(hash, calendars, networkLayer, validateCalendarUrl, logger, signal, minimumSuccessfulSubmissions = 2) {
694
+ if (calendars.length === 0) {
695
+ throw new ValidationError("at least one calendar is required to stamp");
696
+ }
697
+ if (!Number.isInteger(minimumSuccessfulSubmissions) || minimumSuccessfulSubmissions < 1) {
698
+ throw new ValidationError("minimumSuccessfulSubmissions must be an integer >= 1");
699
+ }
700
+ if (minimumSuccessfulSubmissions > calendars.length) {
701
+ throw new ValidationError(
702
+ `minimumSuccessfulSubmissions (${minimumSuccessfulSubmissions}) cannot exceed the number of calendars (${calendars.length})`
703
+ );
704
+ }
705
+ await Promise.all(calendars.map((url) => validateCalendarUrl(url)));
706
+ const digest = validateHash(hash);
707
+ logger?.info(`Starting stamp for ${bytesToHex2(digest)}`);
708
+ const detached = import_core3.DetachedTimestampFile.fromHash(new import_core3.OpSHA256(), digest);
709
+ const nonceAppended = detached.timestamp.add(new import_core3.OpAppend(secureNonce(16)));
710
+ const merkleRoot = nonceAppended.add(new import_core3.OpSHA256());
711
+ const merkleTip = (0, import_core3.makeMerkleTree)([merkleRoot]);
712
+ const results = await Promise.allSettled(
713
+ calendars.map(
714
+ (url) => new CalendarClient(url, networkLayer, logger).submit(merkleTip.getDigest(), signal)
715
+ )
716
+ );
717
+ const successful = [];
718
+ const failed = [];
719
+ results.forEach((r, i) => {
720
+ const calendar = calendars[i];
721
+ if (r.status === "fulfilled") {
722
+ merkleTip.merge(r.value);
723
+ successful.push({ calendar });
724
+ logger?.info(`Submitted to ${calendar}`);
725
+ } else {
726
+ const error = r.reason instanceof Error ? r.reason : new Error(String(r.reason));
727
+ failed.push({ calendar, error });
728
+ logger?.warn(`Failed to submit to ${calendar}: ${error.message}`);
729
+ }
730
+ });
731
+ if (successful.length < minimumSuccessfulSubmissions) {
732
+ throw new StampError(
733
+ `Insufficient successful submissions (${successful.length}/${minimumSuccessfulSubmissions} required)`,
734
+ successful,
735
+ failed
736
+ );
737
+ }
738
+ return detached.serializeToBytes();
739
+ }
740
+
741
+ // src/core/upgrade.ts
742
+ var import_core4 = require("@otskit/core");
743
+ var bytesEqFast = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)) === 0;
744
+ async function orchestrateUpgrade(incompleteProof, _calendars, networkLayer, logger, signal) {
745
+ let detached;
746
+ try {
747
+ detached = import_core4.DetachedTimestampFile.deserialize(new Uint8Array(incompleteProof));
748
+ } catch (error) {
749
+ throw new ValidationError("Invalid .ots proof format", {
750
+ /* v8 ignore next */
751
+ ...error instanceof Error ? { cause: error } : {}
752
+ });
753
+ }
754
+ if (detached.timestamp.isTimestampComplete()) {
755
+ logger?.info("Proof already complete; nothing to upgrade");
756
+ return Buffer.from(incompleteProof);
757
+ }
758
+ const before = detached.serializeToBytes();
759
+ for (const subStamp of detached.timestamp.directlyVerified()) {
760
+ if (subStamp.isTimestampComplete()) continue;
761
+ for (const att of subStamp.attestations) {
762
+ if (att.kind !== "pending") continue;
763
+ if (!DEFAULT_CALENDAR_WHITELIST.contains(att.uri)) {
764
+ logger?.warn(`Ignoring attestation from non-whitelisted calendar ${att.uri}`);
765
+ continue;
766
+ }
767
+ try {
768
+ const upgraded = await new CalendarClient(att.uri, networkLayer, logger).getTimestamp(
769
+ subStamp.getDigest(),
770
+ signal
771
+ );
772
+ subStamp.merge(upgraded);
773
+ } catch (err) {
774
+ if (err instanceof CommitmentNotFoundError) {
775
+ logger?.debug(`Calendar ${att.uri} has not confirmed yet`);
776
+ continue;
777
+ }
778
+ logger?.warn(
779
+ `Failed to query ${att.uri}: ${err instanceof Error ? err.message : String(err)}`
780
+ );
781
+ }
782
+ }
783
+ }
784
+ const after = detached.serializeToBytes();
785
+ if (bytesEqFast(before, after)) {
786
+ throw new UpgradeError("No calendar has confirmed the timestamp yet (Bitcoin not yet mined)");
787
+ }
788
+ return Buffer.from(after);
789
+ }
790
+
791
+ // src/core/verify.ts
792
+ var import_core6 = require("@otskit/core");
793
+ var import_node_crypto2 = require("crypto");
794
+
649
795
  // src/network/esplora.ts
650
796
  var import_node_crypto = require("crypto");
651
- var import_core3 = require("@otskit/core");
797
+ var import_core5 = require("@otskit/core");
652
798
  var PUBLIC_ESPLORA_URL = "https://blockstream.info/api";
653
799
  var MAX_ESPLORA_RESPONSE_SIZE = 1e5;
654
800
  var RAW_HEADER_SIZE = 80;
@@ -774,15 +920,110 @@ var EsploraClient = class {
774
920
  };
775
921
  async function verifyTimestampAttestation(digest, attestation, explorer, signal) {
776
922
  if (attestation.kind !== "bitcoin" && attestation.kind !== "litecoin") {
777
- throw new import_core3.VerificationError(`cannot verify a '${attestation.kind}' attestation against the chain`);
923
+ throw new import_core5.VerificationError(`cannot verify a '${attestation.kind}' attestation against the chain`);
778
924
  }
779
925
  const hash = await explorer.blockHash(attestation.height, signal);
780
926
  const rawHeader = await explorer.rawBlockHeader(hash, signal);
781
- return (0, import_core3.verifyAgainstRawHeader)(digest, rawHeader);
927
+ return (0, import_core5.verifyAgainstRawHeader)(digest, rawHeader);
782
928
  }
783
929
 
784
- // src/core/orchestration.ts
785
- var import_node_crypto2 = require("crypto");
930
+ // src/core/verify.ts
931
+ var MAX_BITCOIN_ATTESTATIONS = 10;
932
+ function timingSafeEq(a, b) {
933
+ if (a.length !== b.length) return false;
934
+ return (0, import_node_crypto2.timingSafeEqual)(a, b);
935
+ }
936
+ async function orchestrateVerify(proof, networkLayer, originalDataHash, logger, signal, esploraUrl) {
937
+ let detached;
938
+ try {
939
+ detached = import_core6.DetachedTimestampFile.deserialize(new Uint8Array(proof));
940
+ } catch (cause) {
941
+ throw new ValidationError("Invalid .ots proof format", {
942
+ ...cause instanceof Error ? { cause } : {}
943
+ });
944
+ }
945
+ if (detached.fileHashOp instanceof import_core6.OpSHA1 || detached.fileHashOp instanceof import_core6.OpRIPEMD160) {
946
+ return {
947
+ status: "invalid",
948
+ reason: `This proof uses ${detached.fileHashOp.tagName} (a weak hash algorithm). Re-stamp the original file with SHA-256 to get a verifiable proof.`
949
+ };
950
+ }
951
+ if (originalDataHash !== void 0) {
952
+ let expected;
953
+ try {
954
+ expected = validateHash(originalDataHash);
955
+ } catch (err) {
956
+ throw new ValidationError(err instanceof Error ? err.message : "Invalid hash format", {
957
+ ...err instanceof Error ? { cause: err } : {}
958
+ });
959
+ }
960
+ if (!timingSafeEq(expected, detached.fileDigest())) {
961
+ return {
962
+ status: "invalid",
963
+ reason: "File hash does not match proof \u2014 file may have been modified"
964
+ };
965
+ }
966
+ }
967
+ const allBitcoin = detached.timestamp.allAttestations().filter(({ attestation }) => attestation.kind === "bitcoin");
968
+ const seenHeights = /* @__PURE__ */ new Set();
969
+ const deduped = allBitcoin.filter(({ attestation }) => {
970
+ if (attestation.kind !== "bitcoin") return false;
971
+ if (seenHeights.has(attestation.height)) {
972
+ logger?.debug(`Skipping duplicate Bitcoin attestation at height ${attestation.height}`);
973
+ return false;
974
+ }
975
+ seenHeights.add(attestation.height);
976
+ return true;
977
+ });
978
+ const bitcoinAtts = deduped.slice(0, MAX_BITCOIN_ATTESTATIONS);
979
+ if (deduped.length > MAX_BITCOIN_ATTESTATIONS) {
980
+ logger?.warn(
981
+ `Proof has ${deduped.length} unique Bitcoin attestations; verifying only the first ${MAX_BITCOIN_ATTESTATIONS}`
982
+ );
983
+ }
984
+ if (bitcoinAtts.length === 0) {
985
+ const hasLitecoin = detached.timestamp.allAttestations().some(({ attestation }) => attestation.kind === "litecoin");
986
+ return {
987
+ status: "pending",
988
+ reason: hasLitecoin ? "Litecoin-only attestation is not supported by this client" : "No Bitcoin attestation found \u2014 timestamp not yet confirmed"
989
+ };
990
+ }
991
+ const explorer = new EsploraClient(networkLayer, {
992
+ ...esploraUrl !== void 0 ? { url: esploraUrl } : {},
993
+ ...logger !== void 0 ? { logger } : {}
994
+ });
995
+ let lastNetworkError;
996
+ let lastCryptoError;
997
+ for (const { msg, attestation } of bitcoinAtts) {
998
+ if (attestation.kind !== "bitcoin") continue;
999
+ try {
1000
+ const blockTime = await verifyTimestampAttestation(
1001
+ Uint8Array.from(msg).reverse(),
1002
+ attestation,
1003
+ explorer,
1004
+ signal
1005
+ );
1006
+ logger?.info(`Verified against Bitcoin block ${attestation.height}`);
1007
+ return { status: "verified", blockHeight: attestation.height, blockTime };
1008
+ } catch (err) {
1009
+ const message = err instanceof Error ? err.message : String(err);
1010
+ if (err instanceof NetworkError || err instanceof EsploraResponseError) {
1011
+ lastNetworkError = message;
1012
+ logger?.warn(`Network error at block ${attestation.height}: ${message}`);
1013
+ } else {
1014
+ lastCryptoError = message;
1015
+ logger?.warn(`Crypto verification failed at block ${attestation.height}: ${message}`);
1016
+ }
1017
+ }
1018
+ }
1019
+ if (lastCryptoError !== void 0) {
1020
+ return { status: "invalid", reason: `Cryptographic verification failed: ${lastCryptoError}` };
1021
+ }
1022
+ return {
1023
+ status: "network_error",
1024
+ reason: `Could not reach Bitcoin blockchain: ${lastNetworkError ?? "unknown error"}`
1025
+ };
1026
+ }
786
1027
 
787
1028
  // src/security/ssrf.ts
788
1029
  var import_promises = require("dns/promises");
@@ -801,7 +1042,7 @@ var BLOCKED_CIDRS_V4 = [
801
1042
  ];
802
1043
  function ipv4ToUint32(ip) {
803
1044
  const parts = ip.split(".");
804
- return (parseInt(parts[0], 10) << 24 | parseInt(parts[1], 10) << 16 | parseInt(parts[2], 10) << 8 | parseInt(parts[3], 10)) >>> 0;
1045
+ 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;
805
1046
  }
806
1047
  function assertNotPrivateIPv4(ip, calendarUrl) {
807
1048
  const n = ipv4ToUint32(ip);
@@ -880,215 +1121,6 @@ async function assertSafeCalendarUrl(url, options) {
880
1121
  }
881
1122
  }
882
1123
 
883
- // src/core/orchestration.ts
884
- var MAX_BITCOIN_ATTESTATIONS = 10;
885
- function validateHash(hash) {
886
- if (typeof hash === "string") {
887
- const hex = hash.trim().toLowerCase();
888
- if (!/^[0-9a-f]{64}$/.test(hex)) {
889
- throw new ValidationError("Hash must be a 64-character hex string (SHA-256)");
890
- }
891
- return Uint8Array.from(Buffer.from(hex, "hex"));
892
- }
893
- if (hash.length !== 32) {
894
- throw new ValidationError("Hash must be exactly 32 bytes (SHA-256)");
895
- }
896
- return Uint8Array.from(hash);
897
- }
898
- function secureNonce(n) {
899
- const bytes = new Uint8Array(n);
900
- if (!globalThis.crypto?.getRandomValues) {
901
- throw new Error("secure RNG unavailable: globalThis.crypto.getRandomValues is required");
902
- }
903
- globalThis.crypto.getRandomValues(bytes);
904
- return bytes;
905
- }
906
- function timingSafeEq(a, b) {
907
- if (a.length !== b.length) return false;
908
- return (0, import_node_crypto2.timingSafeEqual)(a, b);
909
- }
910
- var bytesEqFast = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)) === 0;
911
- async function orchestrateStamp(hash, calendars, networkLayer, logger, signal, minimumSuccessfulSubmissions = 2, allowPrivateCalendars = false) {
912
- if (calendars.length === 0) {
913
- throw new ValidationError("at least one calendar is required to stamp");
914
- }
915
- if (!Number.isInteger(minimumSuccessfulSubmissions) || minimumSuccessfulSubmissions < 1) {
916
- throw new ValidationError("minimumSuccessfulSubmissions must be an integer >= 1");
917
- }
918
- if (minimumSuccessfulSubmissions > calendars.length) {
919
- throw new ValidationError(
920
- `minimumSuccessfulSubmissions (${minimumSuccessfulSubmissions}) cannot exceed the number of calendars (${calendars.length})`
921
- );
922
- }
923
- await Promise.all(
924
- calendars.map((url) => assertSafeCalendarUrl(url, { allowPrivate: allowPrivateCalendars }))
925
- );
926
- const digest = validateHash(hash);
927
- logger?.info(`Starting stamp for ${Buffer.from(digest).toString("hex")}`);
928
- const detached = import_core4.DetachedTimestampFile.fromHash(new import_core4.OpSHA256(), digest);
929
- const nonceAppended = detached.timestamp.add(new import_core4.OpAppend(secureNonce(16)));
930
- const merkleRoot = nonceAppended.add(new import_core4.OpSHA256());
931
- const merkleTip = (0, import_core4.makeMerkleTree)([merkleRoot]);
932
- const results = await Promise.allSettled(
933
- calendars.map((url) => new CalendarClient(url, networkLayer, logger).submit(merkleTip.getDigest(), signal))
934
- );
935
- const successful = [];
936
- const failed = [];
937
- results.forEach((r, i) => {
938
- const calendar = calendars[i];
939
- if (r.status === "fulfilled") {
940
- merkleTip.merge(r.value);
941
- successful.push({ calendar });
942
- logger?.info(`Submitted to ${calendar}`);
943
- } else {
944
- const error = r.reason instanceof Error ? r.reason : new Error(String(r.reason));
945
- failed.push({ calendar, error });
946
- logger?.warn(`Failed to submit to ${calendar}: ${error.message}`);
947
- }
948
- });
949
- if (successful.length < minimumSuccessfulSubmissions) {
950
- throw new StampError(
951
- `Insufficient successful submissions (${successful.length}/${minimumSuccessfulSubmissions} required)`,
952
- successful,
953
- failed
954
- );
955
- }
956
- return Buffer.from(detached.serializeToBytes());
957
- }
958
- async function orchestrateUpgrade(incompleteProof, _calendars, networkLayer, logger, signal) {
959
- let detached;
960
- try {
961
- detached = import_core4.DetachedTimestampFile.deserialize(new Uint8Array(incompleteProof));
962
- } catch (error) {
963
- throw new ValidationError("Invalid .ots proof format", {
964
- /* v8 ignore next */
965
- ...error instanceof Error ? { cause: error } : {}
966
- });
967
- }
968
- if (detached.timestamp.isTimestampComplete()) {
969
- logger?.info("Proof already complete; nothing to upgrade");
970
- return Buffer.from(incompleteProof);
971
- }
972
- const before = detached.serializeToBytes();
973
- for (const subStamp of detached.timestamp.directlyVerified()) {
974
- if (subStamp.isTimestampComplete()) continue;
975
- for (const att of subStamp.attestations) {
976
- if (att.kind !== "pending") continue;
977
- if (!DEFAULT_CALENDAR_WHITELIST.contains(att.uri)) {
978
- logger?.warn(`Ignoring attestation from non-whitelisted calendar ${att.uri}`);
979
- continue;
980
- }
981
- try {
982
- const upgraded = await new CalendarClient(att.uri, networkLayer, logger).getTimestamp(
983
- subStamp.getDigest(),
984
- signal
985
- );
986
- subStamp.merge(upgraded);
987
- } catch (err) {
988
- if (err instanceof CommitmentNotFoundError) {
989
- logger?.debug(`Calendar ${att.uri} has not confirmed yet`);
990
- continue;
991
- }
992
- logger?.warn(`Failed to query ${att.uri}: ${err instanceof Error ? err.message : String(err)}`);
993
- }
994
- }
995
- }
996
- const after = detached.serializeToBytes();
997
- if (bytesEqFast(before, after)) {
998
- throw new UpgradeError("No calendar has confirmed the timestamp yet (Bitcoin not yet mined)");
999
- }
1000
- return Buffer.from(after);
1001
- }
1002
- async function orchestrateVerify(proof, networkLayer, originalDataHash, logger, signal, esploraUrl) {
1003
- let detached;
1004
- try {
1005
- detached = import_core4.DetachedTimestampFile.deserialize(new Uint8Array(proof));
1006
- } catch (cause) {
1007
- throw new ValidationError("Invalid .ots proof format", {
1008
- ...cause instanceof Error ? { cause } : {}
1009
- });
1010
- }
1011
- if (detached.fileHashOp instanceof import_core4.OpSHA1 || detached.fileHashOp instanceof import_core4.OpRIPEMD160) {
1012
- return {
1013
- status: "invalid",
1014
- reason: `This proof uses ${detached.fileHashOp.tagName} (a weak hash algorithm). Re-stamp the original file with SHA-256 to get a verifiable proof.`
1015
- };
1016
- }
1017
- if (originalDataHash !== void 0) {
1018
- let expected;
1019
- try {
1020
- expected = validateHash(originalDataHash);
1021
- } catch (err) {
1022
- throw new ValidationError(
1023
- err instanceof Error ? err.message : "Invalid hash format",
1024
- { ...err instanceof Error ? { cause: err } : {} }
1025
- );
1026
- }
1027
- if (!timingSafeEq(expected, detached.fileDigest())) {
1028
- return { status: "invalid", reason: "File hash does not match proof \u2014 file may have been modified" };
1029
- }
1030
- }
1031
- const allBitcoin = detached.timestamp.allAttestations().filter(({ attestation }) => attestation.kind === "bitcoin");
1032
- const seenHeights = /* @__PURE__ */ new Set();
1033
- const deduped = allBitcoin.filter(({ attestation }) => {
1034
- if (attestation.kind !== "bitcoin") return false;
1035
- if (seenHeights.has(attestation.height)) {
1036
- logger?.debug(`Skipping duplicate Bitcoin attestation at height ${attestation.height}`);
1037
- return false;
1038
- }
1039
- seenHeights.add(attestation.height);
1040
- return true;
1041
- });
1042
- const bitcoinAtts = deduped.slice(0, MAX_BITCOIN_ATTESTATIONS);
1043
- if (deduped.length > MAX_BITCOIN_ATTESTATIONS) {
1044
- logger?.warn(
1045
- `Proof has ${deduped.length} unique Bitcoin attestations; verifying only the first ${MAX_BITCOIN_ATTESTATIONS}`
1046
- );
1047
- }
1048
- if (bitcoinAtts.length === 0) {
1049
- const hasLitecoin = detached.timestamp.allAttestations().some(({ attestation }) => attestation.kind === "litecoin");
1050
- return {
1051
- status: "pending",
1052
- reason: hasLitecoin ? "Litecoin-only attestation is not supported by this client" : "No Bitcoin attestation found \u2014 timestamp not yet confirmed"
1053
- };
1054
- }
1055
- const explorer = new EsploraClient(networkLayer, {
1056
- ...esploraUrl !== void 0 ? { url: esploraUrl } : {},
1057
- ...logger !== void 0 ? { logger } : {}
1058
- });
1059
- let lastNetworkError;
1060
- let lastCryptoError;
1061
- for (const { msg, attestation } of bitcoinAtts) {
1062
- if (attestation.kind !== "bitcoin") continue;
1063
- try {
1064
- const blockTime = await verifyTimestampAttestation(
1065
- Uint8Array.from(msg).reverse(),
1066
- attestation,
1067
- explorer,
1068
- signal
1069
- );
1070
- logger?.info(`Verified against Bitcoin block ${attestation.height}`);
1071
- return { status: "verified", blockHeight: attestation.height, blockTime };
1072
- } catch (err) {
1073
- const message = err instanceof Error ? err.message : String(err);
1074
- if (err instanceof NetworkError || err instanceof EsploraResponseError) {
1075
- lastNetworkError = message;
1076
- logger?.warn(`Network error at block ${attestation.height}: ${message}`);
1077
- } else {
1078
- lastCryptoError = message;
1079
- logger?.warn(`Crypto verification failed at block ${attestation.height}: ${message}`);
1080
- }
1081
- }
1082
- }
1083
- if (lastCryptoError !== void 0) {
1084
- return { status: "invalid", reason: `Cryptographic verification failed: ${lastCryptoError}` };
1085
- }
1086
- return {
1087
- status: "network_error",
1088
- reason: `Could not reach Bitcoin blockchain: ${lastNetworkError ?? "unknown error"}`
1089
- };
1090
- }
1091
-
1092
1124
  // src/client.ts
1093
1125
  var OpenTimestampsClient = class {
1094
1126
  calendars;
@@ -1100,7 +1132,7 @@ var OpenTimestampsClient = class {
1100
1132
  esploraUrl;
1101
1133
  /**
1102
1134
  * Create a new OpenTimestamps client
1103
- *
1135
+ *
1104
1136
  * @param options Client configuration options
1105
1137
  */
1106
1138
  constructor(options = {}) {
@@ -1146,15 +1178,15 @@ var OpenTimestampsClient = class {
1146
1178
  }
1147
1179
  /**
1148
1180
  * Create a timestamp by submitting a hash to calendar servers
1149
- *
1181
+ *
1150
1182
  * @param hash SHA-256 hash of the data to timestamp (as Buffer or hex string)
1151
1183
  * @param options Operation-specific options
1152
1184
  * @returns Initial .ots proof with pending attestations
1153
- *
1185
+ *
1154
1186
  * @throws {ValidationError} If the hash is invalid
1155
1187
  * @throws {StampError} If submission fails to all calendars
1156
1188
  * @throws {NetworkError} If network errors occur
1157
- *
1189
+ *
1158
1190
  * @example
1159
1191
  * ```typescript
1160
1192
  * const hash = crypto.createHash('sha256').update('my data').digest()
@@ -1164,34 +1196,36 @@ var OpenTimestampsClient = class {
1164
1196
  */
1165
1197
  async stamp(hash, options) {
1166
1198
  const signal = options?.signal || this.globalSignal;
1167
- return orchestrateStamp(
1199
+ const allowPrivate = this.allowPrivateCalendars;
1200
+ const bytes = await orchestrateStamp(
1168
1201
  hash,
1169
1202
  this.calendars,
1170
1203
  this.networkLayer,
1204
+ (url) => assertSafeCalendarUrl(url, { allowPrivate }),
1171
1205
  this.logger,
1172
1206
  signal,
1173
- this.minimumSuccessfulSubmissions,
1174
- this.allowPrivateCalendars
1207
+ this.minimumSuccessfulSubmissions
1175
1208
  );
1209
+ return Buffer.from(bytes);
1176
1210
  }
1177
1211
  /**
1178
1212
  * Upgrade an incomplete timestamp proof by querying calendars for Bitcoin confirmation
1179
- *
1213
+ *
1180
1214
  * @param incompleteProof The initial .ots proof returned by stamp()
1181
1215
  * @param options Operation-specific options
1182
1216
  * @returns Upgraded .ots proof with Bitcoin attestation (if available)
1183
- *
1217
+ *
1184
1218
  * @throws {ValidationError} If the proof format is invalid
1185
1219
  * @throws {UpgradeError} If no calendar has confirmed the timestamp yet
1186
1220
  * @throws {NetworkError} If network errors occur
1187
- *
1221
+ *
1188
1222
  * @example
1189
1223
  * ```typescript
1190
1224
  * // Proof already has pending attestations from stamp()
1191
1225
  * const upgradedProof = await client.upgrade(incompleteProof)
1192
- *
1226
+ *
1193
1227
  * // If upgrade throws UpgradeError, Bitcoin hasn't confirmed yet
1194
- * // Retry later (typically 10-60 minutes after stamp)
1228
+ * // Retry later (typically ~60 minutes after stamp)
1195
1229
  * ```
1196
1230
  */
1197
1231
  async upgrade(incompleteProof, options) {
@@ -1206,15 +1240,15 @@ var OpenTimestampsClient = class {
1206
1240
  }
1207
1241
  /**
1208
1242
  * Verify a complete timestamp proof against the Bitcoin blockchain
1209
- *
1243
+ *
1210
1244
  * @param proof The complete .ots proof with Bitcoin attestation
1211
1245
  * @param originalDataHash Optional: the original data hash to verify against
1212
1246
  * @returns Verification result with block details
1213
- *
1247
+ *
1214
1248
  * @example
1215
1249
  * ```typescript
1216
1250
  * const result = await client.verify(completeProof, originalHash)
1217
- *
1251
+ *
1218
1252
  * if (result.valid) {
1219
1253
  * console.log(`Timestamp confirmed in Bitcoin block ${result.blockHeight}`)
1220
1254
  * console.log(`Block timestamp: ${new Date(result.timestamp! * 1000)}`)
@@ -1224,12 +1258,19 @@ var OpenTimestampsClient = class {
1224
1258
  * ```
1225
1259
  */
1226
1260
  async verify(proof, originalDataHash) {
1227
- return orchestrateVerify(proof, this.networkLayer, originalDataHash, this.logger, this.globalSignal, this.esploraUrl);
1261
+ return orchestrateVerify(
1262
+ proof,
1263
+ this.networkLayer,
1264
+ originalDataHash,
1265
+ this.logger,
1266
+ this.globalSignal,
1267
+ this.esploraUrl
1268
+ );
1228
1269
  }
1229
1270
  /**
1230
1271
  * Get the current state of the circuit breaker for a calendar
1231
1272
  * Useful for monitoring and debugging
1232
- *
1273
+ *
1233
1274
  * @param calendarUrl The calendar URL to check
1234
1275
  * @returns Circuit state: 'CLOSED', 'OPEN', or 'HALF_OPEN' (undefined if not yet initialized)
1235
1276
  */
@@ -1239,7 +1280,7 @@ var OpenTimestampsClient = class {
1239
1280
  /**
1240
1281
  * Reset the circuit breaker for a specific calendar
1241
1282
  * Use this to manually recover a calendar that has been marked as failing
1242
- *
1283
+ *
1243
1284
  * @param calendarUrl The calendar URL to reset
1244
1285
  */
1245
1286
  resetCircuit(calendarUrl) {
@@ -1257,19 +1298,19 @@ var OpenTimestampsClient = class {
1257
1298
  };
1258
1299
 
1259
1300
  // src/index.ts
1260
- var import_core5 = require("@otskit/core");
1261
- var import_core6 = require("@otskit/core");
1301
+ var import_core7 = require("@otskit/core");
1302
+ var import_core8 = require("@otskit/core");
1262
1303
 
1263
1304
  // src/utils/hash.ts
1264
- var import_crypto = require("crypto");
1265
- var import_fs = require("fs");
1305
+ var import_node_crypto3 = require("crypto");
1306
+ var import_node_fs = require("fs");
1266
1307
  function hashBuffer(data) {
1267
- return (0, import_crypto.createHash)("sha256").update(data).digest();
1308
+ return (0, import_node_crypto3.createHash)("sha256").update(data).digest();
1268
1309
  }
1269
1310
  function hashFile(path) {
1270
1311
  return new Promise((resolve, reject) => {
1271
- const hash = (0, import_crypto.createHash)("sha256");
1272
- (0, import_fs.createReadStream)(path).on("data", (chunk) => hash.update(chunk)).on("end", () => resolve(hash.digest())).on("error", reject);
1312
+ const hash = (0, import_node_crypto3.createHash)("sha256");
1313
+ (0, import_node_fs.createReadStream)(path).on("data", (chunk) => hash.update(chunk)).on("end", () => resolve(hash.digest())).on("error", reject);
1273
1314
  });
1274
1315
  }
1275
1316
  // Annotate the CommonJS export names for ESM import in node: