@otskit/client 0.4.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/README.md +9 -7
- package/dist/browser.d.ts +36 -0
- package/dist/browser.js +742 -0
- package/dist/index.cjs +378 -267
- package/dist/index.d.cts +71 -56
- package/dist/index.d.ts +71 -56
- package/dist/index.js +374 -268
- package/package.json +18 -8
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
|
-
|
|
32
|
+
if (options?.cause !== void 0)
|
|
33
|
+
this.cause = options.cause;
|
|
33
34
|
Error.captureStackTrace?.(this, this.constructor);
|
|
34
35
|
}
|
|
35
36
|
};
|
|
@@ -47,11 +48,11 @@ var StampError = class extends OpenTimestampsClientError {
|
|
|
47
48
|
var UpgradeError = class extends OpenTimestampsClientError {
|
|
48
49
|
};
|
|
49
50
|
var NetworkError = class extends OpenTimestampsClientError {
|
|
50
|
-
/** HTTP status code
|
|
51
|
+
/** HTTP status code when the failure originates from an HTTP response. */
|
|
51
52
|
status;
|
|
52
53
|
constructor(message, options) {
|
|
53
54
|
super(message, options);
|
|
54
|
-
this.status = options
|
|
55
|
+
if (options?.status !== void 0) this.status = options.status;
|
|
55
56
|
}
|
|
56
57
|
};
|
|
57
58
|
var CircuitBreakerError = class extends NetworkError {
|
|
@@ -74,7 +75,7 @@ var SizeLimitExceededError = class extends NetworkError {
|
|
|
74
75
|
options
|
|
75
76
|
);
|
|
76
77
|
this.maxBytes = maxBytes;
|
|
77
|
-
this.actualBytes = actualBytes;
|
|
78
|
+
if (actualBytes !== void 0) this.actualBytes = actualBytes;
|
|
78
79
|
}
|
|
79
80
|
};
|
|
80
81
|
|
|
@@ -126,7 +127,10 @@ var CircuitBreaker = class {
|
|
|
126
127
|
this.onSuccess(key, circuit);
|
|
127
128
|
return result;
|
|
128
129
|
} catch (error) {
|
|
129
|
-
|
|
130
|
+
const is4xx = error instanceof Error && error.retryable === false;
|
|
131
|
+
if (!is4xx) {
|
|
132
|
+
this.onFailure(key, circuit);
|
|
133
|
+
}
|
|
130
134
|
throw error;
|
|
131
135
|
}
|
|
132
136
|
}
|
|
@@ -223,11 +227,15 @@ function sleep(ms, signal) {
|
|
|
223
227
|
reject(new Error("Aborted"));
|
|
224
228
|
return;
|
|
225
229
|
}
|
|
226
|
-
const
|
|
227
|
-
signal?.addEventListener("abort", () => {
|
|
230
|
+
const onAbort = () => {
|
|
228
231
|
clearTimeout(timeout);
|
|
229
232
|
reject(new Error("Aborted"));
|
|
230
|
-
}
|
|
233
|
+
};
|
|
234
|
+
const timeout = setTimeout(() => {
|
|
235
|
+
signal?.removeEventListener("abort", onAbort);
|
|
236
|
+
resolve();
|
|
237
|
+
}, ms);
|
|
238
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
231
239
|
});
|
|
232
240
|
}
|
|
233
241
|
async function withRetry(fn, options, logger, signal) {
|
|
@@ -310,8 +318,9 @@ async function executeRequest(request, maxBytes) {
|
|
|
310
318
|
const response = await globalThis.fetch(request.url, {
|
|
311
319
|
method: request.method,
|
|
312
320
|
headers: { "Content-Type": "application/octet-stream", ...request.headers },
|
|
313
|
-
body: request.body,
|
|
314
|
-
signal: request.signal
|
|
321
|
+
...request.body !== void 0 ? { body: request.body } : {},
|
|
322
|
+
...request.signal !== void 0 ? { signal: request.signal } : {},
|
|
323
|
+
redirect: "error"
|
|
315
324
|
});
|
|
316
325
|
const data = await readResponseBody(response, maxBytes);
|
|
317
326
|
return { ok: response.ok, status: response.status, statusText: response.statusText, data };
|
|
@@ -320,7 +329,8 @@ async function executeRequest(request, maxBytes) {
|
|
|
320
329
|
if (error instanceof NetworkError) throw error;
|
|
321
330
|
if (error instanceof Error) {
|
|
322
331
|
if (error.name === "AbortError") throw new NetworkError("Request aborted", { cause: error });
|
|
323
|
-
if (error.message.includes("timeout"))
|
|
332
|
+
if (error.message.includes("timeout"))
|
|
333
|
+
throw new NetworkError("Request timeout", { cause: error });
|
|
324
334
|
throw new NetworkError(`Network request failed: ${error.message}`, { cause: error });
|
|
325
335
|
}
|
|
326
336
|
throw new NetworkError("Unknown network error");
|
|
@@ -334,10 +344,14 @@ function createTimeoutController(timeoutMs, parentSignal) {
|
|
|
334
344
|
parentSignal?.removeEventListener("abort", onParentAbort);
|
|
335
345
|
controller.abort(parentSignal?.reason);
|
|
336
346
|
};
|
|
337
|
-
controller.signal.addEventListener(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
347
|
+
controller.signal.addEventListener(
|
|
348
|
+
"abort",
|
|
349
|
+
() => {
|
|
350
|
+
clearTimeout(timeout);
|
|
351
|
+
parentSignal?.removeEventListener("abort", onParentAbort);
|
|
352
|
+
},
|
|
353
|
+
{ once: true }
|
|
354
|
+
);
|
|
341
355
|
if (parentSignal) {
|
|
342
356
|
if (parentSignal.aborted) {
|
|
343
357
|
clearTimeout(timeout);
|
|
@@ -429,13 +443,8 @@ var ResilientNetworkLayer = class {
|
|
|
429
443
|
}
|
|
430
444
|
};
|
|
431
445
|
|
|
432
|
-
// src/core/
|
|
433
|
-
import {
|
|
434
|
-
DetachedTimestampFile,
|
|
435
|
-
OpSHA256,
|
|
436
|
-
OpAppend,
|
|
437
|
-
makeMerkleTree
|
|
438
|
-
} from "@otskit/core";
|
|
446
|
+
// src/core/stamp.ts
|
|
447
|
+
import { DetachedTimestampFile, OpSHA256, OpAppend, makeMerkleTree } from "@otskit/core";
|
|
439
448
|
|
|
440
449
|
// src/network/calendar.ts
|
|
441
450
|
import {
|
|
@@ -470,7 +479,7 @@ var CalendarClient = class {
|
|
|
470
479
|
url;
|
|
471
480
|
networkLayer;
|
|
472
481
|
logger;
|
|
473
|
-
/**
|
|
482
|
+
/** Submits a digest to the calendar and returns the Timestamp that commits to it. */
|
|
474
483
|
async submit(digest, signal) {
|
|
475
484
|
assertCommitment(digest);
|
|
476
485
|
this.logger?.debug(`Submitting digest to ${this.url}/digest`);
|
|
@@ -481,7 +490,7 @@ var CalendarClient = class {
|
|
|
481
490
|
);
|
|
482
491
|
return this.#parseTimestamp(response.data, digest);
|
|
483
492
|
}
|
|
484
|
-
/**
|
|
493
|
+
/** Asks the calendar for a more complete Timestamp for `commitment` (upgrade). */
|
|
485
494
|
async getTimestamp(commitment, signal) {
|
|
486
495
|
assertCommitment(commitment);
|
|
487
496
|
const path = `/timestamp/${bytesToHex(commitment)}`;
|
|
@@ -503,7 +512,7 @@ var CalendarClient = class {
|
|
|
503
512
|
}
|
|
504
513
|
return this.#parseTimestamp(response.data, commitment);
|
|
505
514
|
}
|
|
506
|
-
/**
|
|
515
|
+
/** Deserializes the calendar response as a Timestamp committed to `commitment`. */
|
|
507
516
|
#parseTimestamp(data, commitment) {
|
|
508
517
|
if (data.length > MAX_CALENDAR_RESPONSE_SIZE) {
|
|
509
518
|
throw new CalendarResponseTooLargeError(
|
|
@@ -533,7 +542,7 @@ function parseWhitelistPattern(raw) {
|
|
|
533
542
|
hostname,
|
|
534
543
|
port: parsed.port,
|
|
535
544
|
pathname: parsed.pathname,
|
|
536
|
-
wildcardSuffix
|
|
545
|
+
...wildcardSuffix !== void 0 ? { wildcardSuffix } : {}
|
|
537
546
|
};
|
|
538
547
|
}
|
|
539
548
|
function hostnameMatchesPattern(hostname, pattern) {
|
|
@@ -549,20 +558,26 @@ var UrlWhitelist = class {
|
|
|
549
558
|
for (const u of urls) this.add(u);
|
|
550
559
|
}
|
|
551
560
|
}
|
|
552
|
-
/**
|
|
561
|
+
/**
|
|
562
|
+
* Adds a pattern. If the URL has no scheme, both http:// and https:// variants are added.
|
|
563
|
+
* Throws TypeError if the pattern is not a valid string or is structurally invalid.
|
|
564
|
+
*/
|
|
553
565
|
add(url) {
|
|
554
566
|
if (typeof url !== "string") {
|
|
555
567
|
throw new TypeError("UrlWhitelist: URL must be a string");
|
|
556
568
|
}
|
|
557
569
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
558
570
|
const pattern = parseWhitelistPattern(url);
|
|
559
|
-
if (pattern
|
|
571
|
+
if (pattern === void 0) {
|
|
572
|
+
throw new TypeError(`UrlWhitelist: invalid or unsupported pattern: "${url}"`);
|
|
573
|
+
}
|
|
574
|
+
this.#patterns.set(url, pattern);
|
|
560
575
|
} else {
|
|
561
576
|
this.add("http://" + url);
|
|
562
577
|
this.add("https://" + url);
|
|
563
578
|
}
|
|
564
579
|
}
|
|
565
|
-
/**
|
|
580
|
+
/** Returns true if `url` matches any pattern in the allowlist. */
|
|
566
581
|
contains(url) {
|
|
567
582
|
let parsed;
|
|
568
583
|
try {
|
|
@@ -587,11 +602,158 @@ var UrlWhitelist = class {
|
|
|
587
602
|
var DEFAULT_CALENDAR_WHITELIST = new UrlWhitelist([...TRUSTED_CALENDAR_WHITELIST_PATTERNS]);
|
|
588
603
|
var DEFAULT_AGGREGATORS = [...DEFAULT_AGGREGATOR_URLS];
|
|
589
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
|
+
|
|
590
745
|
// src/network/esplora.ts
|
|
591
|
-
import {
|
|
746
|
+
import { createHash } from "crypto";
|
|
747
|
+
import { verifyAgainstRawHeader, VerificationError } from "@otskit/core";
|
|
592
748
|
var PUBLIC_ESPLORA_URL = "https://blockstream.info/api";
|
|
593
749
|
var MAX_ESPLORA_RESPONSE_SIZE = 1e5;
|
|
750
|
+
var RAW_HEADER_SIZE = 80;
|
|
594
751
|
var HEX64_RE = /^[0-9a-f]{64}$/i;
|
|
752
|
+
function sha256dDisplayHex(data) {
|
|
753
|
+
const first = createHash("sha256").update(data).digest();
|
|
754
|
+
const second = createHash("sha256").update(first).digest();
|
|
755
|
+
return Buffer.from(second).reverse().toString("hex");
|
|
756
|
+
}
|
|
595
757
|
var EsploraClient = class {
|
|
596
758
|
#url;
|
|
597
759
|
#networkLayer;
|
|
@@ -611,7 +773,7 @@ var EsploraClient = class {
|
|
|
611
773
|
this.#url = raw.replace(/\/+$/, "");
|
|
612
774
|
this.#logger = options.logger;
|
|
613
775
|
}
|
|
614
|
-
/**
|
|
776
|
+
/** Returns the block hash (64-char hex, lowercase) at the given height. */
|
|
615
777
|
async blockHash(height, signal) {
|
|
616
778
|
if (!Number.isSafeInteger(height) || height < 0) {
|
|
617
779
|
throw new ValidationError(`block height must be a non-negative safe integer; got ${height}`);
|
|
@@ -628,7 +790,7 @@ var EsploraClient = class {
|
|
|
628
790
|
}
|
|
629
791
|
return text.toLowerCase();
|
|
630
792
|
}
|
|
631
|
-
/**
|
|
793
|
+
/** Returns the block header (merkle root + timestamp) for the given hash. */
|
|
632
794
|
async block(hash, signal) {
|
|
633
795
|
if (typeof hash !== "string" || !HEX64_RE.test(hash)) {
|
|
634
796
|
throw new ValidationError("block hash must be a 64-char hex string");
|
|
@@ -646,7 +808,7 @@ var EsploraClient = class {
|
|
|
646
808
|
} catch (err) {
|
|
647
809
|
throw new EsploraResponseError("esplora returned a non-JSON block response", {
|
|
648
810
|
/* v8 ignore next */
|
|
649
|
-
|
|
811
|
+
...err instanceof Error ? { cause: err } : {}
|
|
650
812
|
});
|
|
651
813
|
}
|
|
652
814
|
if (typeof body !== "object" || body === null) {
|
|
@@ -661,7 +823,36 @@ var EsploraClient = class {
|
|
|
661
823
|
}
|
|
662
824
|
return { merkleroot, time };
|
|
663
825
|
}
|
|
664
|
-
/**
|
|
826
|
+
/**
|
|
827
|
+
* Fetches the raw 80-byte block header for `hash` and self-authenticates it:
|
|
828
|
+
* sha256d(rawHeader) reversed must equal `hash`. This removes trust in the explorer's
|
|
829
|
+
* JSON layer — the raw header is cryptographically bound to the block hash we requested.
|
|
830
|
+
*/
|
|
831
|
+
async rawBlockHeader(hash, signal) {
|
|
832
|
+
if (typeof hash !== "string" || !HEX64_RE.test(hash)) {
|
|
833
|
+
throw new ValidationError("block hash must be a 64-char hex string");
|
|
834
|
+
}
|
|
835
|
+
this.#logger?.debug(`Esplora raw header ${hash}`);
|
|
836
|
+
const response = await this.#networkLayer.request(
|
|
837
|
+
this.#url,
|
|
838
|
+
{ url: `${this.#url}/block/${hash}/header`, method: "GET", headers: { Accept: "application/octet-stream" } },
|
|
839
|
+
signal
|
|
840
|
+
);
|
|
841
|
+
const data = response.data;
|
|
842
|
+
if (data.length !== RAW_HEADER_SIZE) {
|
|
843
|
+
throw new EsploraResponseError(
|
|
844
|
+
`raw block header must be ${RAW_HEADER_SIZE} bytes; got ${data.length}`
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
const actualHash = sha256dDisplayHex(data);
|
|
848
|
+
if (actualHash !== hash.toLowerCase()) {
|
|
849
|
+
throw new EsploraResponseError(
|
|
850
|
+
`raw block header hash mismatch: expected ${hash.toLowerCase()}, got ${actualHash}`
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
return data;
|
|
854
|
+
}
|
|
855
|
+
/** Decodes the response body as text, enforcing the size limit (fail-closed). */
|
|
665
856
|
#decode(data) {
|
|
666
857
|
if (data.length > MAX_ESPLORA_RESPONSE_SIZE) {
|
|
667
858
|
throw new EsploraResponseError(
|
|
@@ -672,7 +863,7 @@ var EsploraClient = class {
|
|
|
672
863
|
return new TextDecoder("utf-8", { fatal: true }).decode(data);
|
|
673
864
|
} catch (cause) {
|
|
674
865
|
throw new EsploraResponseError("esplora response contains invalid UTF-8 bytes", {
|
|
675
|
-
cause
|
|
866
|
+
...cause instanceof Error ? { cause } : {}
|
|
676
867
|
});
|
|
677
868
|
}
|
|
678
869
|
}
|
|
@@ -682,12 +873,107 @@ async function verifyTimestampAttestation(digest, attestation, explorer, signal)
|
|
|
682
873
|
throw new VerificationError(`cannot verify a '${attestation.kind}' attestation against the chain`);
|
|
683
874
|
}
|
|
684
875
|
const hash = await explorer.blockHash(attestation.height, signal);
|
|
685
|
-
const
|
|
686
|
-
return
|
|
876
|
+
const rawHeader = await explorer.rawBlockHeader(hash, signal);
|
|
877
|
+
return verifyAgainstRawHeader(digest, rawHeader);
|
|
687
878
|
}
|
|
688
879
|
|
|
689
|
-
// src/core/
|
|
690
|
-
|
|
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
|
+
}
|
|
691
977
|
|
|
692
978
|
// src/security/ssrf.ts
|
|
693
979
|
import { lookup } from "dns/promises";
|
|
@@ -706,7 +992,7 @@ var BLOCKED_CIDRS_V4 = [
|
|
|
706
992
|
];
|
|
707
993
|
function ipv4ToUint32(ip) {
|
|
708
994
|
const parts = ip.split(".");
|
|
709
|
-
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;
|
|
710
996
|
}
|
|
711
997
|
function assertNotPrivateIPv4(ip, calendarUrl) {
|
|
712
998
|
const n = ipv4ToUint32(ip);
|
|
@@ -785,206 +1071,6 @@ async function assertSafeCalendarUrl(url, options) {
|
|
|
785
1071
|
}
|
|
786
1072
|
}
|
|
787
1073
|
|
|
788
|
-
// src/core/orchestration.ts
|
|
789
|
-
var MAX_BITCOIN_ATTESTATIONS = 10;
|
|
790
|
-
function validateHash(hash) {
|
|
791
|
-
if (typeof hash === "string") {
|
|
792
|
-
const hex = hash.trim().toLowerCase();
|
|
793
|
-
if (!/^[0-9a-f]{64}$/.test(hex)) {
|
|
794
|
-
throw new ValidationError("Hash must be a 64-character hex string (SHA-256)");
|
|
795
|
-
}
|
|
796
|
-
return Uint8Array.from(Buffer.from(hex, "hex"));
|
|
797
|
-
}
|
|
798
|
-
if (hash.length !== 32) {
|
|
799
|
-
throw new ValidationError("Hash must be exactly 32 bytes (SHA-256)");
|
|
800
|
-
}
|
|
801
|
-
return Uint8Array.from(hash);
|
|
802
|
-
}
|
|
803
|
-
function secureNonce(n) {
|
|
804
|
-
const bytes = new Uint8Array(n);
|
|
805
|
-
if (!globalThis.crypto?.getRandomValues) {
|
|
806
|
-
throw new Error("secure RNG unavailable: globalThis.crypto.getRandomValues is required");
|
|
807
|
-
}
|
|
808
|
-
globalThis.crypto.getRandomValues(bytes);
|
|
809
|
-
return bytes;
|
|
810
|
-
}
|
|
811
|
-
function timingSafeEq(a, b) {
|
|
812
|
-
if (a.length !== b.length) return false;
|
|
813
|
-
return timingSafeEqual(a, b);
|
|
814
|
-
}
|
|
815
|
-
var bytesEqFast = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)) === 0;
|
|
816
|
-
async function orchestrateStamp(hash, calendars, networkLayer, logger, signal, minimumSuccessfulSubmissions = 2, allowPrivateCalendars = false) {
|
|
817
|
-
if (calendars.length === 0) {
|
|
818
|
-
throw new ValidationError("at least one calendar is required to stamp");
|
|
819
|
-
}
|
|
820
|
-
if (!Number.isInteger(minimumSuccessfulSubmissions) || minimumSuccessfulSubmissions < 1) {
|
|
821
|
-
throw new ValidationError("minimumSuccessfulSubmissions must be an integer >= 1");
|
|
822
|
-
}
|
|
823
|
-
if (minimumSuccessfulSubmissions > calendars.length) {
|
|
824
|
-
throw new ValidationError(
|
|
825
|
-
`minimumSuccessfulSubmissions (${minimumSuccessfulSubmissions}) cannot exceed the number of calendars (${calendars.length})`
|
|
826
|
-
);
|
|
827
|
-
}
|
|
828
|
-
await Promise.all(
|
|
829
|
-
calendars.map((url) => assertSafeCalendarUrl(url, { allowPrivate: allowPrivateCalendars }))
|
|
830
|
-
);
|
|
831
|
-
const digest = validateHash(hash);
|
|
832
|
-
logger?.info(`Starting stamp for ${Buffer.from(digest).toString("hex")}`);
|
|
833
|
-
const detached = DetachedTimestampFile.fromHash(new OpSHA256(), digest);
|
|
834
|
-
const nonceAppended = detached.timestamp.add(new OpAppend(secureNonce(16)));
|
|
835
|
-
const merkleRoot = nonceAppended.add(new OpSHA256());
|
|
836
|
-
const merkleTip = makeMerkleTree([merkleRoot]);
|
|
837
|
-
const results = await Promise.allSettled(
|
|
838
|
-
calendars.map((url) => new CalendarClient(url, networkLayer, logger).submit(merkleTip.getDigest(), signal))
|
|
839
|
-
);
|
|
840
|
-
const successful = [];
|
|
841
|
-
const failed = [];
|
|
842
|
-
results.forEach((r, i) => {
|
|
843
|
-
const calendar = calendars[i];
|
|
844
|
-
if (r.status === "fulfilled") {
|
|
845
|
-
merkleTip.merge(r.value);
|
|
846
|
-
successful.push({ calendar });
|
|
847
|
-
logger?.info(`Submitted to ${calendar}`);
|
|
848
|
-
} else {
|
|
849
|
-
const error = r.reason instanceof Error ? r.reason : new Error(String(r.reason));
|
|
850
|
-
failed.push({ calendar, error });
|
|
851
|
-
logger?.warn(`Failed to submit to ${calendar}: ${error.message}`);
|
|
852
|
-
}
|
|
853
|
-
});
|
|
854
|
-
if (successful.length < minimumSuccessfulSubmissions) {
|
|
855
|
-
throw new StampError(
|
|
856
|
-
`Insufficient successful submissions (${successful.length}/${minimumSuccessfulSubmissions} required)`,
|
|
857
|
-
successful,
|
|
858
|
-
failed
|
|
859
|
-
);
|
|
860
|
-
}
|
|
861
|
-
return Buffer.from(detached.serializeToBytes());
|
|
862
|
-
}
|
|
863
|
-
async function orchestrateUpgrade(incompleteProof, _calendars, networkLayer, logger, signal) {
|
|
864
|
-
let detached;
|
|
865
|
-
try {
|
|
866
|
-
detached = DetachedTimestampFile.deserialize(new Uint8Array(incompleteProof));
|
|
867
|
-
} catch (error) {
|
|
868
|
-
throw new ValidationError("Invalid .ots proof format", {
|
|
869
|
-
/* v8 ignore next */
|
|
870
|
-
cause: error instanceof Error ? error : void 0
|
|
871
|
-
});
|
|
872
|
-
}
|
|
873
|
-
if (detached.timestamp.isTimestampComplete()) {
|
|
874
|
-
logger?.info("Proof already complete; nothing to upgrade");
|
|
875
|
-
return Buffer.from(incompleteProof);
|
|
876
|
-
}
|
|
877
|
-
const before = detached.serializeToBytes();
|
|
878
|
-
for (const subStamp of detached.timestamp.directlyVerified()) {
|
|
879
|
-
if (subStamp.isTimestampComplete()) continue;
|
|
880
|
-
for (const att of subStamp.attestations) {
|
|
881
|
-
if (att.kind !== "pending") continue;
|
|
882
|
-
if (!DEFAULT_CALENDAR_WHITELIST.contains(att.uri)) {
|
|
883
|
-
logger?.warn(`Ignoring attestation from non-whitelisted calendar ${att.uri}`);
|
|
884
|
-
continue;
|
|
885
|
-
}
|
|
886
|
-
try {
|
|
887
|
-
const upgraded = await new CalendarClient(att.uri, networkLayer, logger).getTimestamp(
|
|
888
|
-
subStamp.getDigest(),
|
|
889
|
-
signal
|
|
890
|
-
);
|
|
891
|
-
subStamp.merge(upgraded);
|
|
892
|
-
} catch (err) {
|
|
893
|
-
if (err instanceof CommitmentNotFoundError) {
|
|
894
|
-
logger?.debug(`Calendar ${att.uri} has not confirmed yet`);
|
|
895
|
-
continue;
|
|
896
|
-
}
|
|
897
|
-
logger?.warn(`Failed to query ${att.uri}: ${err instanceof Error ? err.message : String(err)}`);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
const after = detached.serializeToBytes();
|
|
902
|
-
if (bytesEqFast(before, after)) {
|
|
903
|
-
throw new UpgradeError("No calendar has confirmed the timestamp yet (Bitcoin not yet mined)");
|
|
904
|
-
}
|
|
905
|
-
return Buffer.from(after);
|
|
906
|
-
}
|
|
907
|
-
async function orchestrateVerify(proof, networkLayer, originalDataHash, logger, signal) {
|
|
908
|
-
let detached;
|
|
909
|
-
try {
|
|
910
|
-
detached = DetachedTimestampFile.deserialize(new Uint8Array(proof));
|
|
911
|
-
} catch (cause) {
|
|
912
|
-
throw new ValidationError("Invalid .ots proof format", {
|
|
913
|
-
cause: cause instanceof Error ? cause : void 0
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
if (originalDataHash !== void 0) {
|
|
917
|
-
let expected;
|
|
918
|
-
try {
|
|
919
|
-
expected = validateHash(originalDataHash);
|
|
920
|
-
} catch (err) {
|
|
921
|
-
throw new ValidationError(
|
|
922
|
-
err instanceof Error ? err.message : "Invalid hash format",
|
|
923
|
-
{ cause: err instanceof Error ? err : void 0 }
|
|
924
|
-
);
|
|
925
|
-
}
|
|
926
|
-
if (!timingSafeEq(expected, detached.fileDigest())) {
|
|
927
|
-
return { status: "invalid", reason: "File hash does not match proof \u2014 file may have been modified" };
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
const allBitcoin = detached.timestamp.allAttestations().filter(({ attestation }) => attestation.kind === "bitcoin");
|
|
931
|
-
const seenHeights = /* @__PURE__ */ new Set();
|
|
932
|
-
const deduped = allBitcoin.filter(({ attestation }) => {
|
|
933
|
-
if (attestation.kind !== "bitcoin") return false;
|
|
934
|
-
if (seenHeights.has(attestation.height)) {
|
|
935
|
-
logger?.debug(`Skipping duplicate Bitcoin attestation at height ${attestation.height}`);
|
|
936
|
-
return false;
|
|
937
|
-
}
|
|
938
|
-
seenHeights.add(attestation.height);
|
|
939
|
-
return true;
|
|
940
|
-
});
|
|
941
|
-
const bitcoinAtts = deduped.slice(0, MAX_BITCOIN_ATTESTATIONS);
|
|
942
|
-
if (deduped.length > MAX_BITCOIN_ATTESTATIONS) {
|
|
943
|
-
logger?.warn(
|
|
944
|
-
`Proof has ${deduped.length} unique Bitcoin attestations; verifying only the first ${MAX_BITCOIN_ATTESTATIONS}`
|
|
945
|
-
);
|
|
946
|
-
}
|
|
947
|
-
if (bitcoinAtts.length === 0) {
|
|
948
|
-
const hasLitecoin = detached.timestamp.allAttestations().some(({ attestation }) => attestation.kind === "litecoin");
|
|
949
|
-
return {
|
|
950
|
-
status: "pending",
|
|
951
|
-
reason: hasLitecoin ? "Litecoin-only attestation is not supported by this client" : "No Bitcoin attestation found \u2014 timestamp not yet confirmed"
|
|
952
|
-
};
|
|
953
|
-
}
|
|
954
|
-
const explorer = new EsploraClient(networkLayer);
|
|
955
|
-
let lastNetworkError;
|
|
956
|
-
let lastCryptoError;
|
|
957
|
-
for (const { msg, attestation } of bitcoinAtts) {
|
|
958
|
-
if (attestation.kind !== "bitcoin") continue;
|
|
959
|
-
try {
|
|
960
|
-
const blockTime = await verifyTimestampAttestation(
|
|
961
|
-
Uint8Array.from(msg).reverse(),
|
|
962
|
-
attestation,
|
|
963
|
-
explorer,
|
|
964
|
-
signal
|
|
965
|
-
);
|
|
966
|
-
logger?.info(`Verified against Bitcoin block ${attestation.height}`);
|
|
967
|
-
return { status: "verified", blockHeight: attestation.height, blockTime };
|
|
968
|
-
} catch (err) {
|
|
969
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
970
|
-
if (err instanceof NetworkError || err instanceof EsploraResponseError) {
|
|
971
|
-
lastNetworkError = message;
|
|
972
|
-
logger?.warn(`Network error at block ${attestation.height}: ${message}`);
|
|
973
|
-
} else {
|
|
974
|
-
lastCryptoError = message;
|
|
975
|
-
logger?.warn(`Crypto verification failed at block ${attestation.height}: ${message}`);
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
if (lastCryptoError !== void 0) {
|
|
980
|
-
return { status: "invalid", reason: `Cryptographic verification failed: ${lastCryptoError}` };
|
|
981
|
-
}
|
|
982
|
-
return {
|
|
983
|
-
status: "network_error",
|
|
984
|
-
reason: `Could not reach Bitcoin blockchain: ${lastNetworkError ?? "unknown error"}`
|
|
985
|
-
};
|
|
986
|
-
}
|
|
987
|
-
|
|
988
1074
|
// src/client.ts
|
|
989
1075
|
var OpenTimestampsClient = class {
|
|
990
1076
|
calendars;
|
|
@@ -993,20 +1079,32 @@ var OpenTimestampsClient = class {
|
|
|
993
1079
|
globalSignal;
|
|
994
1080
|
minimumSuccessfulSubmissions;
|
|
995
1081
|
allowPrivateCalendars;
|
|
1082
|
+
esploraUrl;
|
|
996
1083
|
/**
|
|
997
1084
|
* Create a new OpenTimestamps client
|
|
998
|
-
*
|
|
1085
|
+
*
|
|
999
1086
|
* @param options Client configuration options
|
|
1000
1087
|
*/
|
|
1001
1088
|
constructor(options = {}) {
|
|
1089
|
+
this.logger = options.logger;
|
|
1002
1090
|
if (!options.calendars || options.calendars.length === 0) {
|
|
1003
1091
|
this.calendars = DEFAULT_CALENDARS;
|
|
1004
1092
|
this.logger?.info("No calendars provided, using defaults");
|
|
1005
1093
|
} else {
|
|
1006
1094
|
this.calendars = options.calendars;
|
|
1007
1095
|
}
|
|
1008
|
-
|
|
1096
|
+
const minSubs = options.minimumSuccessfulSubmissions ?? 2;
|
|
1097
|
+
if (!Number.isInteger(minSubs) || minSubs < 1) {
|
|
1098
|
+
throw new ValidationError("minimumSuccessfulSubmissions must be an integer >= 1");
|
|
1099
|
+
}
|
|
1100
|
+
if (minSubs > this.calendars.length) {
|
|
1101
|
+
throw new ValidationError(
|
|
1102
|
+
`minimumSuccessfulSubmissions (${minSubs}) cannot exceed the number of calendars (${this.calendars.length})`
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
this.minimumSuccessfulSubmissions = minSubs;
|
|
1009
1106
|
this.allowPrivateCalendars = options.allowPrivateCalendars ?? false;
|
|
1107
|
+
this.esploraUrl = options.esploraUrl;
|
|
1010
1108
|
const resilienceConfig = {
|
|
1011
1109
|
...DEFAULT_RESILIENCE,
|
|
1012
1110
|
...options.resilience,
|
|
@@ -1023,23 +1121,22 @@ var OpenTimestampsClient = class {
|
|
|
1023
1121
|
...options.resilience?.circuitBreaker
|
|
1024
1122
|
}
|
|
1025
1123
|
};
|
|
1026
|
-
this.
|
|
1027
|
-
this.globalSignal = options.signal;
|
|
1124
|
+
if (options.signal !== void 0) this.globalSignal = options.signal;
|
|
1028
1125
|
const internalOptions = options;
|
|
1029
1126
|
this.networkLayer = internalOptions._networkLayer ?? new ResilientNetworkLayer(resilienceConfig, this.logger);
|
|
1030
1127
|
this.logger?.info(`OpenTimestamps client initialized with ${this.calendars.length} calendars`);
|
|
1031
1128
|
}
|
|
1032
1129
|
/**
|
|
1033
1130
|
* Create a timestamp by submitting a hash to calendar servers
|
|
1034
|
-
*
|
|
1131
|
+
*
|
|
1035
1132
|
* @param hash SHA-256 hash of the data to timestamp (as Buffer or hex string)
|
|
1036
1133
|
* @param options Operation-specific options
|
|
1037
1134
|
* @returns Initial .ots proof with pending attestations
|
|
1038
|
-
*
|
|
1135
|
+
*
|
|
1039
1136
|
* @throws {ValidationError} If the hash is invalid
|
|
1040
1137
|
* @throws {StampError} If submission fails to all calendars
|
|
1041
1138
|
* @throws {NetworkError} If network errors occur
|
|
1042
|
-
*
|
|
1139
|
+
*
|
|
1043
1140
|
* @example
|
|
1044
1141
|
* ```typescript
|
|
1045
1142
|
* const hash = crypto.createHash('sha256').update('my data').digest()
|
|
@@ -1049,34 +1146,36 @@ var OpenTimestampsClient = class {
|
|
|
1049
1146
|
*/
|
|
1050
1147
|
async stamp(hash, options) {
|
|
1051
1148
|
const signal = options?.signal || this.globalSignal;
|
|
1052
|
-
|
|
1149
|
+
const allowPrivate = this.allowPrivateCalendars;
|
|
1150
|
+
const bytes = await orchestrateStamp(
|
|
1053
1151
|
hash,
|
|
1054
1152
|
this.calendars,
|
|
1055
1153
|
this.networkLayer,
|
|
1154
|
+
(url) => assertSafeCalendarUrl(url, { allowPrivate }),
|
|
1056
1155
|
this.logger,
|
|
1057
1156
|
signal,
|
|
1058
|
-
this.minimumSuccessfulSubmissions
|
|
1059
|
-
this.allowPrivateCalendars
|
|
1157
|
+
this.minimumSuccessfulSubmissions
|
|
1060
1158
|
);
|
|
1159
|
+
return Buffer.from(bytes);
|
|
1061
1160
|
}
|
|
1062
1161
|
/**
|
|
1063
1162
|
* Upgrade an incomplete timestamp proof by querying calendars for Bitcoin confirmation
|
|
1064
|
-
*
|
|
1163
|
+
*
|
|
1065
1164
|
* @param incompleteProof The initial .ots proof returned by stamp()
|
|
1066
1165
|
* @param options Operation-specific options
|
|
1067
1166
|
* @returns Upgraded .ots proof with Bitcoin attestation (if available)
|
|
1068
|
-
*
|
|
1167
|
+
*
|
|
1069
1168
|
* @throws {ValidationError} If the proof format is invalid
|
|
1070
1169
|
* @throws {UpgradeError} If no calendar has confirmed the timestamp yet
|
|
1071
1170
|
* @throws {NetworkError} If network errors occur
|
|
1072
|
-
*
|
|
1171
|
+
*
|
|
1073
1172
|
* @example
|
|
1074
1173
|
* ```typescript
|
|
1075
1174
|
* // Proof already has pending attestations from stamp()
|
|
1076
1175
|
* const upgradedProof = await client.upgrade(incompleteProof)
|
|
1077
|
-
*
|
|
1176
|
+
*
|
|
1078
1177
|
* // If upgrade throws UpgradeError, Bitcoin hasn't confirmed yet
|
|
1079
|
-
* // Retry later (typically
|
|
1178
|
+
* // Retry later (typically ~60 minutes after stamp)
|
|
1080
1179
|
* ```
|
|
1081
1180
|
*/
|
|
1082
1181
|
async upgrade(incompleteProof, options) {
|
|
@@ -1091,15 +1190,15 @@ var OpenTimestampsClient = class {
|
|
|
1091
1190
|
}
|
|
1092
1191
|
/**
|
|
1093
1192
|
* Verify a complete timestamp proof against the Bitcoin blockchain
|
|
1094
|
-
*
|
|
1193
|
+
*
|
|
1095
1194
|
* @param proof The complete .ots proof with Bitcoin attestation
|
|
1096
1195
|
* @param originalDataHash Optional: the original data hash to verify against
|
|
1097
1196
|
* @returns Verification result with block details
|
|
1098
|
-
*
|
|
1197
|
+
*
|
|
1099
1198
|
* @example
|
|
1100
1199
|
* ```typescript
|
|
1101
1200
|
* const result = await client.verify(completeProof, originalHash)
|
|
1102
|
-
*
|
|
1201
|
+
*
|
|
1103
1202
|
* if (result.valid) {
|
|
1104
1203
|
* console.log(`Timestamp confirmed in Bitcoin block ${result.blockHeight}`)
|
|
1105
1204
|
* console.log(`Block timestamp: ${new Date(result.timestamp! * 1000)}`)
|
|
@@ -1109,12 +1208,19 @@ var OpenTimestampsClient = class {
|
|
|
1109
1208
|
* ```
|
|
1110
1209
|
*/
|
|
1111
1210
|
async verify(proof, originalDataHash) {
|
|
1112
|
-
return orchestrateVerify(
|
|
1211
|
+
return orchestrateVerify(
|
|
1212
|
+
proof,
|
|
1213
|
+
this.networkLayer,
|
|
1214
|
+
originalDataHash,
|
|
1215
|
+
this.logger,
|
|
1216
|
+
this.globalSignal,
|
|
1217
|
+
this.esploraUrl
|
|
1218
|
+
);
|
|
1113
1219
|
}
|
|
1114
1220
|
/**
|
|
1115
1221
|
* Get the current state of the circuit breaker for a calendar
|
|
1116
1222
|
* Useful for monitoring and debugging
|
|
1117
|
-
*
|
|
1223
|
+
*
|
|
1118
1224
|
* @param calendarUrl The calendar URL to check
|
|
1119
1225
|
* @returns Circuit state: 'CLOSED', 'OPEN', or 'HALF_OPEN' (undefined if not yet initialized)
|
|
1120
1226
|
*/
|
|
@@ -1124,7 +1230,7 @@ var OpenTimestampsClient = class {
|
|
|
1124
1230
|
/**
|
|
1125
1231
|
* Reset the circuit breaker for a specific calendar
|
|
1126
1232
|
* Use this to manually recover a calendar that has been marked as failing
|
|
1127
|
-
*
|
|
1233
|
+
*
|
|
1128
1234
|
* @param calendarUrl The calendar URL to reset
|
|
1129
1235
|
*/
|
|
1130
1236
|
resetCircuit(calendarUrl) {
|
|
@@ -1142,18 +1248,18 @@ var OpenTimestampsClient = class {
|
|
|
1142
1248
|
};
|
|
1143
1249
|
|
|
1144
1250
|
// src/index.ts
|
|
1145
|
-
import { DetachedTimestampFile as
|
|
1146
|
-
import { verifyAgainstBlockheader
|
|
1251
|
+
import { DetachedTimestampFile as DetachedTimestampFile4, Timestamp as Timestamp2 } from "@otskit/core";
|
|
1252
|
+
import { verifyAgainstBlockheader } from "@otskit/core";
|
|
1147
1253
|
|
|
1148
1254
|
// src/utils/hash.ts
|
|
1149
|
-
import { createHash } from "crypto";
|
|
1255
|
+
import { createHash as createHash2 } from "crypto";
|
|
1150
1256
|
import { createReadStream } from "fs";
|
|
1151
1257
|
function hashBuffer(data) {
|
|
1152
|
-
return
|
|
1258
|
+
return createHash2("sha256").update(data).digest();
|
|
1153
1259
|
}
|
|
1154
1260
|
function hashFile(path) {
|
|
1155
1261
|
return new Promise((resolve, reject) => {
|
|
1156
|
-
const hash =
|
|
1262
|
+
const hash = createHash2("sha256");
|
|
1157
1263
|
createReadStream(path).on("data", (chunk) => hash.update(chunk)).on("end", () => resolve(hash.digest())).on("error", reject);
|
|
1158
1264
|
});
|
|
1159
1265
|
}
|
|
@@ -1167,7 +1273,7 @@ export {
|
|
|
1167
1273
|
DEFAULT_CALENDARS,
|
|
1168
1274
|
DEFAULT_CALENDAR_WHITELIST,
|
|
1169
1275
|
DEFAULT_RESILIENCE,
|
|
1170
|
-
|
|
1276
|
+
DetachedTimestampFile4 as DetachedTimestampFile,
|
|
1171
1277
|
EsploraClient,
|
|
1172
1278
|
EsploraResponseError,
|
|
1173
1279
|
MAX_CALENDAR_RESPONSE_SIZE,
|
|
@@ -1187,6 +1293,6 @@ export {
|
|
|
1187
1293
|
hashBuffer,
|
|
1188
1294
|
hashFile,
|
|
1189
1295
|
isVerified,
|
|
1190
|
-
|
|
1296
|
+
verifyAgainstBlockheader,
|
|
1191
1297
|
verifyTimestampAttestation
|
|
1192
1298
|
};
|