@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.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;
@@ -739,10 +885,18 @@ var EsploraClient = class {
739
885
  this.#logger?.debug(`Esplora raw header ${hash}`);
740
886
  const response = await this.#networkLayer.request(
741
887
  this.#url,
742
- { url: `${this.#url}/block/${hash}/header`, method: "GET", headers: { Accept: "application/octet-stream" } },
888
+ { url: `${this.#url}/block/${hash}/header`, method: "GET", headers: { Accept: "text/plain" } },
743
889
  signal
744
890
  );
745
- const data = response.data;
891
+ const hex = this.#decode(response.data).trim();
892
+ let data;
893
+ try {
894
+ data = hexToBytes(hex);
895
+ } catch (cause) {
896
+ throw new EsploraResponseError("esplora returned a non-hex raw block header", {
897
+ ...cause instanceof Error ? { cause } : {}
898
+ });
899
+ }
746
900
  if (data.length !== RAW_HEADER_SIZE) {
747
901
  throw new EsploraResponseError(
748
902
  `raw block header must be ${RAW_HEADER_SIZE} bytes; got ${data.length}`
@@ -774,15 +928,105 @@ var EsploraClient = class {
774
928
  };
775
929
  async function verifyTimestampAttestation(digest, attestation, explorer, signal) {
776
930
  if (attestation.kind !== "bitcoin" && attestation.kind !== "litecoin") {
777
- throw new import_core3.VerificationError(`cannot verify a '${attestation.kind}' attestation against the chain`);
931
+ throw new import_core5.VerificationError(`cannot verify a '${attestation.kind}' attestation against the chain`);
778
932
  }
779
933
  const hash = await explorer.blockHash(attestation.height, signal);
780
934
  const rawHeader = await explorer.rawBlockHeader(hash, signal);
781
- return (0, import_core3.verifyAgainstRawHeader)(digest, rawHeader);
935
+ return (0, import_core5.verifyAgainstRawHeader)(digest, rawHeader);
782
936
  }
783
937
 
784
- // src/core/orchestration.ts
785
- var import_node_crypto2 = require("crypto");
938
+ // src/core/verify.ts
939
+ var MAX_BITCOIN_ATTESTATIONS = 10;
940
+ function timingSafeEq(a, b) {
941
+ if (a.length !== b.length) return false;
942
+ return (0, import_node_crypto2.timingSafeEqual)(a, b);
943
+ }
944
+ async function orchestrateVerify(proof, networkLayer, originalDataHash, logger, signal, esploraUrl) {
945
+ let detached;
946
+ try {
947
+ detached = import_core6.DetachedTimestampFile.deserialize(new Uint8Array(proof));
948
+ } catch (cause) {
949
+ throw new ValidationError("Invalid .ots proof format", {
950
+ ...cause instanceof Error ? { cause } : {}
951
+ });
952
+ }
953
+ if (detached.fileHashOp instanceof import_core6.OpSHA1 || detached.fileHashOp instanceof import_core6.OpRIPEMD160) {
954
+ return {
955
+ status: "invalid",
956
+ reason: `This proof uses ${detached.fileHashOp.tagName} (a weak hash algorithm). Re-stamp the original file with SHA-256 to get a verifiable proof.`
957
+ };
958
+ }
959
+ if (originalDataHash !== void 0) {
960
+ let expected;
961
+ try {
962
+ expected = validateHash(originalDataHash);
963
+ } catch (err) {
964
+ throw new ValidationError(err instanceof Error ? err.message : "Invalid hash format", {
965
+ ...err instanceof Error ? { cause: err } : {}
966
+ });
967
+ }
968
+ if (!timingSafeEq(expected, detached.fileDigest())) {
969
+ return {
970
+ status: "invalid",
971
+ reason: "File hash does not match proof \u2014 file may have been modified"
972
+ };
973
+ }
974
+ }
975
+ const allBitcoin = detached.timestamp.allAttestations().filter(({ attestation }) => attestation.kind === "bitcoin");
976
+ const seenHeights = /* @__PURE__ */ new Set();
977
+ const deduped = allBitcoin.filter(({ attestation }) => {
978
+ if (attestation.kind !== "bitcoin") return false;
979
+ if (seenHeights.has(attestation.height)) {
980
+ logger?.debug(`Skipping duplicate Bitcoin attestation at height ${attestation.height}`);
981
+ return false;
982
+ }
983
+ seenHeights.add(attestation.height);
984
+ return true;
985
+ });
986
+ const bitcoinAtts = deduped.slice(0, MAX_BITCOIN_ATTESTATIONS);
987
+ if (deduped.length > MAX_BITCOIN_ATTESTATIONS) {
988
+ logger?.warn(
989
+ `Proof has ${deduped.length} unique Bitcoin attestations; verifying only the first ${MAX_BITCOIN_ATTESTATIONS}`
990
+ );
991
+ }
992
+ if (bitcoinAtts.length === 0) {
993
+ const hasLitecoin = detached.timestamp.allAttestations().some(({ attestation }) => attestation.kind === "litecoin");
994
+ return {
995
+ status: "pending",
996
+ reason: hasLitecoin ? "Litecoin-only attestation is not supported by this client" : "No Bitcoin attestation found \u2014 timestamp not yet confirmed"
997
+ };
998
+ }
999
+ const explorer = new EsploraClient(networkLayer, {
1000
+ ...esploraUrl !== void 0 ? { url: esploraUrl } : {},
1001
+ ...logger !== void 0 ? { logger } : {}
1002
+ });
1003
+ let lastNetworkError;
1004
+ let lastCryptoError;
1005
+ for (const { msg, attestation } of bitcoinAtts) {
1006
+ if (attestation.kind !== "bitcoin") continue;
1007
+ try {
1008
+ const blockTime = await verifyTimestampAttestation(msg, attestation, explorer, signal);
1009
+ logger?.info(`Verified against Bitcoin block ${attestation.height}`);
1010
+ return { status: "verified", blockHeight: attestation.height, blockTime };
1011
+ } catch (err) {
1012
+ const message = err instanceof Error ? err.message : String(err);
1013
+ if (err instanceof NetworkError || err instanceof EsploraResponseError) {
1014
+ lastNetworkError = message;
1015
+ logger?.warn(`Network error at block ${attestation.height}: ${message}`);
1016
+ } else {
1017
+ lastCryptoError = message;
1018
+ logger?.warn(`Crypto verification failed at block ${attestation.height}: ${message}`);
1019
+ }
1020
+ }
1021
+ }
1022
+ if (lastCryptoError !== void 0) {
1023
+ return { status: "invalid", reason: `Cryptographic verification failed: ${lastCryptoError}` };
1024
+ }
1025
+ return {
1026
+ status: "network_error",
1027
+ reason: `Could not reach Bitcoin blockchain: ${lastNetworkError ?? "unknown error"}`
1028
+ };
1029
+ }
786
1030
 
787
1031
  // src/security/ssrf.ts
788
1032
  var import_promises = require("dns/promises");
@@ -801,7 +1045,7 @@ var BLOCKED_CIDRS_V4 = [
801
1045
  ];
802
1046
  function ipv4ToUint32(ip) {
803
1047
  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;
1048
+ 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
1049
  }
806
1050
  function assertNotPrivateIPv4(ip, calendarUrl) {
807
1051
  const n = ipv4ToUint32(ip);
@@ -880,215 +1124,6 @@ async function assertSafeCalendarUrl(url, options) {
880
1124
  }
881
1125
  }
882
1126
 
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
1127
  // src/client.ts
1093
1128
  var OpenTimestampsClient = class {
1094
1129
  calendars;
@@ -1100,7 +1135,7 @@ var OpenTimestampsClient = class {
1100
1135
  esploraUrl;
1101
1136
  /**
1102
1137
  * Create a new OpenTimestamps client
1103
- *
1138
+ *
1104
1139
  * @param options Client configuration options
1105
1140
  */
1106
1141
  constructor(options = {}) {
@@ -1146,15 +1181,15 @@ var OpenTimestampsClient = class {
1146
1181
  }
1147
1182
  /**
1148
1183
  * Create a timestamp by submitting a hash to calendar servers
1149
- *
1184
+ *
1150
1185
  * @param hash SHA-256 hash of the data to timestamp (as Buffer or hex string)
1151
1186
  * @param options Operation-specific options
1152
1187
  * @returns Initial .ots proof with pending attestations
1153
- *
1188
+ *
1154
1189
  * @throws {ValidationError} If the hash is invalid
1155
1190
  * @throws {StampError} If submission fails to all calendars
1156
1191
  * @throws {NetworkError} If network errors occur
1157
- *
1192
+ *
1158
1193
  * @example
1159
1194
  * ```typescript
1160
1195
  * const hash = crypto.createHash('sha256').update('my data').digest()
@@ -1164,34 +1199,36 @@ var OpenTimestampsClient = class {
1164
1199
  */
1165
1200
  async stamp(hash, options) {
1166
1201
  const signal = options?.signal || this.globalSignal;
1167
- return orchestrateStamp(
1202
+ const allowPrivate = this.allowPrivateCalendars;
1203
+ const bytes = await orchestrateStamp(
1168
1204
  hash,
1169
1205
  this.calendars,
1170
1206
  this.networkLayer,
1207
+ (url) => assertSafeCalendarUrl(url, { allowPrivate }),
1171
1208
  this.logger,
1172
1209
  signal,
1173
- this.minimumSuccessfulSubmissions,
1174
- this.allowPrivateCalendars
1210
+ this.minimumSuccessfulSubmissions
1175
1211
  );
1212
+ return Buffer.from(bytes);
1176
1213
  }
1177
1214
  /**
1178
1215
  * Upgrade an incomplete timestamp proof by querying calendars for Bitcoin confirmation
1179
- *
1216
+ *
1180
1217
  * @param incompleteProof The initial .ots proof returned by stamp()
1181
1218
  * @param options Operation-specific options
1182
1219
  * @returns Upgraded .ots proof with Bitcoin attestation (if available)
1183
- *
1220
+ *
1184
1221
  * @throws {ValidationError} If the proof format is invalid
1185
1222
  * @throws {UpgradeError} If no calendar has confirmed the timestamp yet
1186
1223
  * @throws {NetworkError} If network errors occur
1187
- *
1224
+ *
1188
1225
  * @example
1189
1226
  * ```typescript
1190
1227
  * // Proof already has pending attestations from stamp()
1191
1228
  * const upgradedProof = await client.upgrade(incompleteProof)
1192
- *
1229
+ *
1193
1230
  * // If upgrade throws UpgradeError, Bitcoin hasn't confirmed yet
1194
- * // Retry later (typically 10-60 minutes after stamp)
1231
+ * // Retry later (typically ~60 minutes after stamp)
1195
1232
  * ```
1196
1233
  */
1197
1234
  async upgrade(incompleteProof, options) {
@@ -1206,15 +1243,15 @@ var OpenTimestampsClient = class {
1206
1243
  }
1207
1244
  /**
1208
1245
  * Verify a complete timestamp proof against the Bitcoin blockchain
1209
- *
1246
+ *
1210
1247
  * @param proof The complete .ots proof with Bitcoin attestation
1211
1248
  * @param originalDataHash Optional: the original data hash to verify against
1212
1249
  * @returns Verification result with block details
1213
- *
1250
+ *
1214
1251
  * @example
1215
1252
  * ```typescript
1216
1253
  * const result = await client.verify(completeProof, originalHash)
1217
- *
1254
+ *
1218
1255
  * if (result.valid) {
1219
1256
  * console.log(`Timestamp confirmed in Bitcoin block ${result.blockHeight}`)
1220
1257
  * console.log(`Block timestamp: ${new Date(result.timestamp! * 1000)}`)
@@ -1224,12 +1261,19 @@ var OpenTimestampsClient = class {
1224
1261
  * ```
1225
1262
  */
1226
1263
  async verify(proof, originalDataHash) {
1227
- return orchestrateVerify(proof, this.networkLayer, originalDataHash, this.logger, this.globalSignal, this.esploraUrl);
1264
+ return orchestrateVerify(
1265
+ proof,
1266
+ this.networkLayer,
1267
+ originalDataHash,
1268
+ this.logger,
1269
+ this.globalSignal,
1270
+ this.esploraUrl
1271
+ );
1228
1272
  }
1229
1273
  /**
1230
1274
  * Get the current state of the circuit breaker for a calendar
1231
1275
  * Useful for monitoring and debugging
1232
- *
1276
+ *
1233
1277
  * @param calendarUrl The calendar URL to check
1234
1278
  * @returns Circuit state: 'CLOSED', 'OPEN', or 'HALF_OPEN' (undefined if not yet initialized)
1235
1279
  */
@@ -1239,7 +1283,7 @@ var OpenTimestampsClient = class {
1239
1283
  /**
1240
1284
  * Reset the circuit breaker for a specific calendar
1241
1285
  * Use this to manually recover a calendar that has been marked as failing
1242
- *
1286
+ *
1243
1287
  * @param calendarUrl The calendar URL to reset
1244
1288
  */
1245
1289
  resetCircuit(calendarUrl) {
@@ -1257,19 +1301,19 @@ var OpenTimestampsClient = class {
1257
1301
  };
1258
1302
 
1259
1303
  // src/index.ts
1260
- var import_core5 = require("@otskit/core");
1261
- var import_core6 = require("@otskit/core");
1304
+ var import_core7 = require("@otskit/core");
1305
+ var import_core8 = require("@otskit/core");
1262
1306
 
1263
1307
  // src/utils/hash.ts
1264
- var import_crypto = require("crypto");
1265
- var import_fs = require("fs");
1308
+ var import_node_crypto3 = require("crypto");
1309
+ var import_node_fs = require("fs");
1266
1310
  function hashBuffer(data) {
1267
- return (0, import_crypto.createHash)("sha256").update(data).digest();
1311
+ return (0, import_node_crypto3.createHash)("sha256").update(data).digest();
1268
1312
  }
1269
1313
  function hashFile(path) {
1270
1314
  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);
1315
+ const hash = (0, import_node_crypto3.createHash)("sha256");
1316
+ (0, import_node_fs.createReadStream)(path).on("data", (chunk) => hash.update(chunk)).on("end", () => resolve(hash.digest())).on("error", reject);
1273
1317
  });
1274
1318
  }
1275
1319
  // Annotate the CommonJS export names for ESM import in node: