@otskit/client 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,7 @@
1
1
  // src/types.ts
2
- var DEFAULT_CALENDARS = [
3
- "https://alice.btc.calendar.opentimestamps.org",
4
- "https://bob.btc.calendar.opentimestamps.org",
5
- "https://finney.calendar.eternitywall.com",
6
- "https://btc.calendar.catallaxy.com"
7
- ];
2
+ import { DEFAULT_CALENDAR_URLS } from "@otskit/core";
3
+ var isVerified = (r) => r.status === "verified";
4
+ var DEFAULT_CALENDARS = [...DEFAULT_CALENDAR_URLS];
8
5
  var DEFAULT_RESILIENCE = {
9
6
  totalTimeoutMs: 3e4,
10
7
  connectTimeoutMs: 5e3,
@@ -68,6 +65,18 @@ var CalendarResponseTooLargeError = class extends NetworkError {
68
65
  };
69
66
  var EsploraResponseError = class extends NetworkError {
70
67
  };
68
+ var SizeLimitExceededError = class extends NetworkError {
69
+ maxBytes;
70
+ actualBytes;
71
+ constructor(maxBytes, actualBytes, options) {
72
+ super(
73
+ actualBytes === void 0 ? `Response size exceeds limit of ${maxBytes} bytes` : `Response size ${actualBytes} bytes exceeds limit of ${maxBytes} bytes`,
74
+ options
75
+ );
76
+ this.maxBytes = maxBytes;
77
+ this.actualBytes = actualBytes;
78
+ }
79
+ };
71
80
 
72
81
  // src/network/circuit-breaker.ts
73
82
  var CircuitState = /* @__PURE__ */ ((CircuitState2) => {
@@ -256,33 +265,62 @@ async function withRetry(fn, options, logger, signal) {
256
265
  }
257
266
 
258
267
  // src/adapters/fetch-adapter.ts
259
- async function executeRequest(request) {
268
+ function getDeclaredContentLength(response) {
269
+ const value = response.headers.get("content-length");
270
+ if (value === null || !/^\d+$/.test(value)) return void 0;
271
+ const n = Number(value);
272
+ return Number.isSafeInteger(n) ? n : void 0;
273
+ }
274
+ async function readStreamLimited(body, maxBytes, status) {
275
+ const reader = body.getReader();
276
+ const buffer = new Uint8Array(maxBytes);
277
+ let received = 0;
278
+ try {
279
+ while (true) {
280
+ const { done, value } = await reader.read();
281
+ if (done) return buffer.subarray(0, received);
282
+ const next = received + value.byteLength;
283
+ if (next > maxBytes) {
284
+ await reader.cancel();
285
+ throw new SizeLimitExceededError(maxBytes, next, { status });
286
+ }
287
+ buffer.set(value, received);
288
+ received = next;
289
+ }
290
+ } finally {
291
+ reader.releaseLock();
292
+ }
293
+ }
294
+ async function readResponseBody(response, maxBytes) {
295
+ const contentLength = getDeclaredContentLength(response);
296
+ if (contentLength !== void 0 && contentLength > maxBytes) {
297
+ throw new SizeLimitExceededError(maxBytes, contentLength, { status: response.status });
298
+ }
299
+ if (response.body === null) {
300
+ const ab = await response.arrayBuffer();
301
+ if (ab.byteLength > maxBytes) {
302
+ throw new SizeLimitExceededError(maxBytes, ab.byteLength, { status: response.status });
303
+ }
304
+ return new Uint8Array(ab);
305
+ }
306
+ return readStreamLimited(response.body, maxBytes, response.status);
307
+ }
308
+ async function executeRequest(request, maxBytes) {
260
309
  try {
261
310
  const response = await globalThis.fetch(request.url, {
262
311
  method: request.method,
263
- headers: {
264
- "Content-Type": "application/octet-stream",
265
- ...request.headers
266
- },
312
+ headers: { "Content-Type": "application/octet-stream", ...request.headers },
267
313
  body: request.body,
268
314
  signal: request.signal
269
315
  });
270
- const arrayBuffer = await response.arrayBuffer();
271
- const data = new Uint8Array(arrayBuffer);
272
- return {
273
- ok: response.ok,
274
- status: response.status,
275
- statusText: response.statusText,
276
- data
277
- };
316
+ const data = await readResponseBody(response, maxBytes);
317
+ return { ok: response.ok, status: response.status, statusText: response.statusText, data };
278
318
  } catch (error) {
319
+ if (error instanceof SizeLimitExceededError) throw error;
320
+ if (error instanceof NetworkError) throw error;
279
321
  if (error instanceof Error) {
280
- if (error.name === "AbortError") {
281
- throw new NetworkError("Request aborted", { cause: error });
282
- }
283
- if (error.message.includes("timeout")) {
284
- throw new NetworkError("Request timeout", { cause: error });
285
- }
322
+ if (error.name === "AbortError") throw new NetworkError("Request aborted", { cause: error });
323
+ if (error.message.includes("timeout")) throw new NetworkError("Request timeout", { cause: error });
286
324
  throw new NetworkError(`Network request failed: ${error.message}`, { cause: error });
287
325
  }
288
326
  throw new NetworkError("Unknown network error");
@@ -290,23 +328,24 @@ async function executeRequest(request) {
290
328
  }
291
329
  function createTimeoutController(timeoutMs, parentSignal) {
292
330
  const controller = new AbortController();
293
- const timeout = setTimeout(() => {
294
- controller.abort(new Error("Timeout"));
295
- }, timeoutMs);
331
+ const timeout = setTimeout(() => controller.abort(new Error("Timeout")), timeoutMs);
332
+ const onParentAbort = () => {
333
+ clearTimeout(timeout);
334
+ parentSignal?.removeEventListener("abort", onParentAbort);
335
+ controller.abort(parentSignal?.reason);
336
+ };
337
+ controller.signal.addEventListener("abort", () => {
338
+ clearTimeout(timeout);
339
+ parentSignal?.removeEventListener("abort", onParentAbort);
340
+ }, { once: true });
296
341
  if (parentSignal) {
297
342
  if (parentSignal.aborted) {
298
343
  clearTimeout(timeout);
299
344
  controller.abort(parentSignal.reason);
300
345
  } else {
301
- parentSignal.addEventListener("abort", () => {
302
- clearTimeout(timeout);
303
- controller.abort(parentSignal.reason);
304
- });
346
+ parentSignal.addEventListener("abort", onParentAbort, { once: true });
305
347
  }
306
348
  }
307
- controller.signal.addEventListener("abort", () => {
308
- clearTimeout(timeout);
309
- });
310
349
  return controller;
311
350
  }
312
351
 
@@ -338,10 +377,10 @@ var ResilientNetworkLayer = class {
338
377
  totalController.signal
339
378
  );
340
379
  try {
341
- const response = await executeRequest({
342
- ...request,
343
- signal: attemptController.signal
344
- });
380
+ const response = await executeRequest(
381
+ { ...request, signal: attemptController.signal },
382
+ this.options.maxResponseBytes ?? 1e5
383
+ );
345
384
  const elapsed = Date.now() - startTime;
346
385
  this.logger?.debug(`Request to ${calendarUrl} succeeded in ${elapsed}ms`);
347
386
  if (!response.ok) {
@@ -360,8 +399,7 @@ var ResilientNetworkLayer = class {
360
399
  }
361
400
  return response;
362
401
  } finally {
363
- attemptController.signal.removeEventListener("abort", () => {
364
- });
402
+ attemptController.abort(new Error("Attempt complete"));
365
403
  }
366
404
  },
367
405
  this.options.retries,
@@ -374,8 +412,7 @@ var ResilientNetworkLayer = class {
374
412
  this.logger?.error(`Request to ${calendarUrl} failed after ${elapsed}ms`, error);
375
413
  throw error;
376
414
  } finally {
377
- totalController.signal.removeEventListener("abort", () => {
378
- });
415
+ totalController.abort(new Error("Request complete"));
379
416
  }
380
417
  }
381
418
  /** Get circuit breaker state for a calendar */
@@ -392,1453 +429,22 @@ var ResilientNetworkLayer = class {
392
429
  }
393
430
  };
394
431
 
395
- // ../otskit-core/dist/index.js
396
- var DeserializationError = class extends Error {
397
- constructor(message) {
398
- super(message);
399
- this.name = new.target.name;
400
- Object.setPrototypeOf(this, new.target.prototype);
401
- }
402
- };
403
- var BadMagicError = class extends DeserializationError {
404
- };
405
- var TruncatedStreamError = class extends DeserializationError {
406
- };
407
- var OversizedDataError = class extends DeserializationError {
408
- };
409
- var VaruintOverflowError = class extends DeserializationError {
410
- };
411
- var TrailingGarbageError = class extends DeserializationError {
412
- };
413
- var UnknownOperationError = class extends DeserializationError {
414
- };
415
- var OpExecutionError = class extends Error {
416
- constructor(message) {
417
- super(message);
418
- this.name = new.target.name;
419
- Object.setPrototypeOf(this, new.target.prototype);
420
- }
421
- };
422
- var MessageTooLongError = class extends OpExecutionError {
423
- };
424
- var ResultTooLongError = class extends OpExecutionError {
425
- };
426
- var InvalidUriError = class extends DeserializationError {
427
- };
428
- var VerificationError = class extends Error {
429
- constructor(message) {
430
- super(message);
431
- this.name = new.target.name;
432
- Object.setPrototypeOf(this, new.target.prototype);
433
- }
434
- };
435
- var EmptyTimestampError = class extends Error {
436
- constructor(message) {
437
- super(message);
438
- this.name = new.target.name;
439
- Object.setPrototypeOf(this, new.target.prototype);
440
- }
441
- };
442
- var MergeError = class extends Error {
443
- constructor(message) {
444
- super(message);
445
- this.name = new.target.name;
446
- Object.setPrototypeOf(this, new.target.prototype);
447
- }
448
- };
449
- var EmptyMerkleTreeError = class extends Error {
450
- constructor(message) {
451
- super(message);
452
- this.name = new.target.name;
453
- Object.setPrototypeOf(this, new.target.prototype);
454
- }
455
- };
456
- var UnsupportedVersionError = class extends DeserializationError {
457
- };
458
- var HEX_RE = /^[0-9a-fA-F]*$/;
459
- function hexToBytes(hex) {
460
- if (hex.length % 2 !== 0) {
461
- throw new Error(`hex string must have even length; got ${hex.length}`);
462
- }
463
- if (!HEX_RE.test(hex)) {
464
- throw new Error("hex string contains non-hex characters");
465
- }
466
- const out = new Uint8Array(hex.length / 2);
467
- for (let i = 0; i < out.length; i++) {
468
- out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
469
- }
470
- return out;
471
- }
472
- var HEX_TABLE = Array.from({ length: 256 }, (_, b) => b.toString(16).padStart(2, "0"));
473
- function bytesToHex(bytes) {
474
- let s = "";
475
- for (let i = 0; i < bytes.length; i++) {
476
- s += HEX_TABLE[bytes[i]];
477
- }
478
- return s;
479
- }
480
- var encoder = new TextEncoder();
481
- var decoder = new TextDecoder("utf-8", { fatal: true });
482
- function textToBytes(text) {
483
- return encoder.encode(text);
484
- }
485
- function bytesEqual(a, b) {
486
- if (a.length !== b.length) return false;
487
- for (let i = 0; i < a.length; i++) {
488
- if (a[i] !== b[i]) return false;
489
- }
490
- return true;
491
- }
492
- function compareBytes(a, b) {
493
- const min = Math.min(a.length, b.length);
494
- for (let i = 0; i < min; i++) {
495
- const d = a[i] - b[i];
496
- if (d !== 0) return d;
497
- }
498
- return a.length - b.length;
499
- }
500
- var StreamDeserializationContext = class {
501
- #buffer;
502
- #counter = 0;
503
- constructor(stream) {
504
- if (!(stream instanceof Uint8Array)) {
505
- throw new TypeError("StreamDeserializationContext expects a Uint8Array");
506
- }
507
- this.#buffer = stream;
508
- }
509
- get counter() {
510
- return this.#counter;
511
- }
512
- /** Lee `length` bytes. Lanza si el stream no tiene suficientes. */
513
- read(length) {
514
- if (length < 0) throw new RangeError("read length must be >= 0");
515
- if (this.#counter + length > this.#buffer.length) {
516
- throw new TruncatedStreamError(
517
- `attempted to read ${length} bytes at offset ${this.#counter}, only ${this.#buffer.length - this.#counter} available`
518
- );
519
- }
520
- const slice = this.#buffer.subarray(this.#counter, this.#counter + length);
521
- this.#counter += length;
522
- return slice;
523
- }
524
- /** Lee un único byte. */
525
- readByte() {
526
- return this.read(1)[0];
527
- }
528
- /** Varuint LEB128: 7 bits por byte, bit 7 = continuación. */
529
- readVaruint() {
530
- let value = 0;
531
- let shift = 0;
532
- let byte;
533
- do {
534
- if (shift > 56) {
535
- throw new VaruintOverflowError("varuint exceeds Number.MAX_SAFE_INTEGER");
536
- }
537
- byte = this.readByte();
538
- value += (byte & 127) * 2 ** shift;
539
- if (!Number.isSafeInteger(value)) {
540
- throw new VaruintOverflowError("varuint exceeds Number.MAX_SAFE_INTEGER");
541
- }
542
- shift += 7;
543
- } while (byte & 128);
544
- return value;
545
- }
546
- /** Lee un bloque varbytes. `maxLen` es obligatorio (defensa DoS). */
547
- readVarbytes(maxLen, minLen = 0) {
548
- const length = this.readVaruint();
549
- if (length > maxLen) {
550
- throw new OversizedDataError(`varbytes length ${length} exceeds maxLen ${maxLen}`);
551
- }
552
- if (length < minLen) {
553
- throw new OversizedDataError(`varbytes length ${length} below minLen ${minLen}`);
554
- }
555
- return this.read(length);
556
- }
557
- /** Verifica el número mágico de cabecera. */
558
- assertMagic(expectedMagic) {
559
- const actual = this.read(expectedMagic.length);
560
- if (!bytesEqual(expectedMagic, actual)) {
561
- throw new BadMagicError("header magic mismatch");
562
- }
563
- }
564
- /** Exige que no queden bytes sin consumir. */
565
- assertEof() {
566
- if (this.#counter < this.#buffer.length) {
567
- throw new TrailingGarbageError("trailing garbage after end of deserialized data");
568
- }
569
- }
570
- };
571
- var StreamSerializationContext = class {
572
- #buffer = new Uint8Array(4096);
573
- #length = 0;
574
- get length() {
575
- return this.#length;
576
- }
577
- getOutput() {
578
- return this.#buffer.slice(0, this.#length);
579
- }
580
- writeByte(value) {
581
- if (!Number.isInteger(value) || value < 0 || value > 255) {
582
- throw new RangeError(`writeByte expects a byte 0..255; got ${value}`);
583
- }
584
- if (this.#length >= this.#buffer.length) {
585
- const grown = new Uint8Array(this.#buffer.length * 2);
586
- grown.set(this.#buffer, 0);
587
- this.#buffer = grown;
588
- }
589
- this.#buffer[this.#length] = value;
590
- this.#length += 1;
591
- }
592
- writeBytes(value) {
593
- for (let i = 0; i < value.length; i++) {
594
- this.writeByte(value[i]);
595
- }
596
- }
597
- /** Codifica un varuint LEB128. */
598
- writeVaruint(value) {
599
- if (!Number.isSafeInteger(value) || value < 0) {
600
- throw new RangeError(`writeVaruint expects a safe non-negative integer; got ${value}`);
601
- }
602
- do {
603
- let byte = value % 128;
604
- value = Math.floor(value / 128);
605
- if (value > 0) byte |= 128;
606
- this.writeByte(byte);
607
- } while (value > 0);
608
- }
609
- writeVarbytes(value) {
610
- this.writeVaruint(value.length);
611
- this.writeBytes(value);
612
- }
613
- };
614
- var rotl = (x, n) => x << n | x >>> 32 - n;
615
- function sha1(msg) {
616
- let h0 = 1732584193, h1 = 4023233417, h2 = 2562383102, h3 = 271733878, h4 = 3285377520;
617
- const bitLen = msg.length * 8;
618
- const withOne = msg.length + 1;
619
- const padded = new Uint8Array(Math.ceil((withOne + 8) / 64) * 64);
620
- padded.set(msg, 0);
621
- padded[msg.length] = 128;
622
- const dv = new DataView(padded.buffer);
623
- dv.setUint32(padded.length - 4, bitLen >>> 0, false);
624
- dv.setUint32(padded.length - 8, Math.floor(bitLen / 2 ** 32), false);
625
- const w = new Uint32Array(80);
626
- for (let off = 0; off < padded.length; off += 64) {
627
- for (let i = 0; i < 16; i++) w[i] = dv.getUint32(off + i * 4, false);
628
- for (let i = 16; i < 80; i++) w[i] = rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
629
- let a = h0, b = h1, c = h2, d = h3, e = h4;
630
- for (let i = 0; i < 80; i++) {
631
- let f2, k;
632
- if (i < 20) {
633
- f2 = b & c | ~b & d;
634
- k = 1518500249;
635
- } else if (i < 40) {
636
- f2 = b ^ c ^ d;
637
- k = 1859775393;
638
- } else if (i < 60) {
639
- f2 = b & c | b & d | c & d;
640
- k = 2400959708;
641
- } else {
642
- f2 = b ^ c ^ d;
643
- k = 3395469782;
644
- }
645
- const t = rotl(a, 5) + f2 + e + k + w[i] | 0;
646
- e = d;
647
- d = c;
648
- c = rotl(b, 30);
649
- b = a;
650
- a = t;
651
- }
652
- h0 = h0 + a | 0;
653
- h1 = h1 + b | 0;
654
- h2 = h2 + c | 0;
655
- h3 = h3 + d | 0;
656
- h4 = h4 + e | 0;
657
- }
658
- const out = new Uint8Array(20);
659
- const odv = new DataView(out.buffer);
660
- odv.setUint32(0, h0, false);
661
- odv.setUint32(4, h1, false);
662
- odv.setUint32(8, h2, false);
663
- odv.setUint32(12, h3, false);
664
- odv.setUint32(16, h4, false);
665
- return out;
666
- }
667
- var K = new Uint32Array([
668
- 1116352408,
669
- 1899447441,
670
- 3049323471,
671
- 3921009573,
672
- 961987163,
673
- 1508970993,
674
- 2453635748,
675
- 2870763221,
676
- 3624381080,
677
- 310598401,
678
- 607225278,
679
- 1426881987,
680
- 1925078388,
681
- 2162078206,
682
- 2614888103,
683
- 3248222580,
684
- 3835390401,
685
- 4022224774,
686
- 264347078,
687
- 604807628,
688
- 770255983,
689
- 1249150122,
690
- 1555081692,
691
- 1996064986,
692
- 2554220882,
693
- 2821834349,
694
- 2952996808,
695
- 3210313671,
696
- 3336571891,
697
- 3584528711,
698
- 113926993,
699
- 338241895,
700
- 666307205,
701
- 773529912,
702
- 1294757372,
703
- 1396182291,
704
- 1695183700,
705
- 1986661051,
706
- 2177026350,
707
- 2456956037,
708
- 2730485921,
709
- 2820302411,
710
- 3259730800,
711
- 3345764771,
712
- 3516065817,
713
- 3600352804,
714
- 4094571909,
715
- 275423344,
716
- 430227734,
717
- 506948616,
718
- 659060556,
719
- 883997877,
720
- 958139571,
721
- 1322822218,
722
- 1537002063,
723
- 1747873779,
724
- 1955562222,
725
- 2024104815,
726
- 2227730452,
727
- 2361852424,
728
- 2428436474,
729
- 2756734187,
730
- 3204031479,
731
- 3329325298
732
- ]);
733
- var rotr = (x, n) => x >>> n | x << 32 - n;
734
- function sha256(msg) {
735
- const h = new Uint32Array([
736
- 1779033703,
737
- 3144134277,
738
- 1013904242,
739
- 2773480762,
740
- 1359893119,
741
- 2600822924,
742
- 528734635,
743
- 1541459225
744
- ]);
745
- const bitLen = msg.length * 8;
746
- const withOne = msg.length + 1;
747
- const padded = new Uint8Array(Math.ceil((withOne + 8) / 64) * 64);
748
- padded.set(msg, 0);
749
- padded[msg.length] = 128;
750
- const dv = new DataView(padded.buffer);
751
- dv.setUint32(padded.length - 4, bitLen >>> 0, false);
752
- dv.setUint32(padded.length - 8, Math.floor(bitLen / 2 ** 32), false);
753
- const w = new Uint32Array(64);
754
- for (let off = 0; off < padded.length; off += 64) {
755
- for (let i = 0; i < 16; i++) w[i] = dv.getUint32(off + i * 4, false);
756
- for (let i = 16; i < 64; i++) {
757
- const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
758
- const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
759
- w[i] = w[i - 16] + s0 + w[i - 7] + s1 | 0;
760
- }
761
- let [a, b, c, d, e, f2, g, hh] = h;
762
- for (let i = 0; i < 64; i++) {
763
- const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
764
- const ch = e & f2 ^ ~e & g;
765
- const t1 = hh + S1 + ch + K[i] + w[i] | 0;
766
- const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
767
- const maj = a & b ^ a & c ^ b & c;
768
- const t2 = S0 + maj | 0;
769
- hh = g;
770
- g = f2;
771
- f2 = e;
772
- e = d + t1 | 0;
773
- d = c;
774
- c = b;
775
- b = a;
776
- a = t1 + t2 | 0;
777
- }
778
- h[0] = h[0] + a | 0;
779
- h[1] = h[1] + b | 0;
780
- h[2] = h[2] + c | 0;
781
- h[3] = h[3] + d | 0;
782
- h[4] = h[4] + e | 0;
783
- h[5] = h[5] + f2 | 0;
784
- h[6] = h[6] + g | 0;
785
- h[7] = h[7] + hh | 0;
786
- }
787
- const out = new Uint8Array(32);
788
- const odv = new DataView(out.buffer);
789
- for (let i = 0; i < 8; i++) odv.setUint32(i * 4, h[i], false);
790
- return out;
791
- }
792
- var rol = (x, n) => x << n | x >>> 32 - n;
793
- var ZL = [
794
- 0,
795
- 1,
796
- 2,
797
- 3,
798
- 4,
799
- 5,
800
- 6,
801
- 7,
802
- 8,
803
- 9,
804
- 10,
805
- 11,
806
- 12,
807
- 13,
808
- 14,
809
- 15,
810
- 7,
811
- 4,
812
- 13,
813
- 1,
814
- 10,
815
- 6,
816
- 15,
817
- 3,
818
- 12,
819
- 0,
820
- 9,
821
- 5,
822
- 2,
823
- 14,
824
- 11,
825
- 8,
826
- 3,
827
- 10,
828
- 14,
829
- 4,
830
- 9,
831
- 15,
832
- 8,
833
- 1,
834
- 2,
835
- 7,
836
- 0,
837
- 6,
838
- 13,
839
- 11,
840
- 5,
841
- 12,
842
- 1,
843
- 9,
844
- 11,
845
- 10,
846
- 0,
847
- 8,
848
- 12,
849
- 4,
850
- 13,
851
- 3,
852
- 7,
853
- 15,
854
- 14,
855
- 5,
856
- 6,
857
- 2,
858
- 4,
859
- 0,
860
- 5,
861
- 9,
862
- 7,
863
- 12,
864
- 2,
865
- 10,
866
- 14,
867
- 1,
868
- 3,
869
- 8,
870
- 11,
871
- 6,
872
- 15,
873
- 13
874
- ];
875
- var ZR = [
876
- 5,
877
- 14,
878
- 7,
879
- 0,
880
- 9,
881
- 2,
882
- 11,
883
- 4,
884
- 13,
885
- 6,
886
- 15,
887
- 8,
888
- 1,
889
- 10,
890
- 3,
891
- 12,
892
- 6,
893
- 11,
894
- 3,
895
- 7,
896
- 0,
897
- 13,
898
- 5,
899
- 10,
900
- 14,
901
- 15,
902
- 8,
903
- 12,
904
- 4,
905
- 9,
906
- 1,
907
- 2,
908
- 15,
909
- 5,
910
- 1,
911
- 3,
912
- 7,
913
- 14,
914
- 6,
915
- 9,
916
- 11,
917
- 8,
918
- 12,
919
- 2,
920
- 10,
921
- 0,
922
- 4,
923
- 13,
924
- 8,
925
- 6,
926
- 4,
927
- 1,
928
- 3,
929
- 11,
930
- 15,
931
- 0,
932
- 5,
933
- 12,
934
- 2,
935
- 13,
936
- 9,
937
- 7,
938
- 10,
939
- 14,
940
- 12,
941
- 15,
942
- 10,
943
- 4,
944
- 1,
945
- 5,
946
- 8,
947
- 7,
948
- 6,
949
- 2,
950
- 13,
951
- 14,
952
- 0,
953
- 3,
954
- 9,
955
- 11
956
- ];
957
- var SL = [
958
- 11,
959
- 14,
960
- 15,
961
- 12,
962
- 5,
963
- 8,
964
- 7,
965
- 9,
966
- 11,
967
- 13,
968
- 14,
969
- 15,
970
- 6,
971
- 7,
972
- 9,
973
- 8,
974
- 7,
975
- 6,
976
- 8,
977
- 13,
978
- 11,
979
- 9,
980
- 7,
981
- 15,
982
- 7,
983
- 12,
984
- 15,
985
- 9,
986
- 11,
987
- 7,
988
- 13,
989
- 12,
990
- 11,
991
- 13,
992
- 6,
993
- 7,
994
- 14,
995
- 9,
996
- 13,
997
- 15,
998
- 14,
999
- 8,
1000
- 13,
1001
- 6,
1002
- 5,
1003
- 12,
1004
- 7,
1005
- 5,
1006
- 11,
1007
- 12,
1008
- 14,
1009
- 15,
1010
- 14,
1011
- 15,
1012
- 9,
1013
- 8,
1014
- 9,
1015
- 14,
1016
- 5,
1017
- 6,
1018
- 8,
1019
- 6,
1020
- 5,
1021
- 12,
1022
- 9,
1023
- 15,
1024
- 5,
1025
- 11,
1026
- 6,
1027
- 8,
1028
- 13,
1029
- 12,
1030
- 5,
1031
- 12,
1032
- 13,
1033
- 14,
1034
- 11,
1035
- 8,
1036
- 5,
1037
- 6
1038
- ];
1039
- var SR = [
1040
- 8,
1041
- 9,
1042
- 9,
1043
- 11,
1044
- 13,
1045
- 15,
1046
- 15,
1047
- 5,
1048
- 7,
1049
- 7,
1050
- 8,
1051
- 11,
1052
- 14,
1053
- 14,
1054
- 12,
1055
- 6,
1056
- 9,
1057
- 13,
1058
- 15,
1059
- 7,
1060
- 12,
1061
- 8,
1062
- 9,
1063
- 11,
1064
- 7,
1065
- 7,
1066
- 12,
1067
- 7,
1068
- 6,
1069
- 15,
1070
- 13,
1071
- 11,
1072
- 9,
1073
- 7,
1074
- 15,
1075
- 11,
1076
- 8,
1077
- 6,
1078
- 6,
1079
- 14,
1080
- 12,
1081
- 13,
1082
- 5,
1083
- 14,
1084
- 13,
1085
- 13,
1086
- 7,
1087
- 5,
1088
- 15,
1089
- 5,
1090
- 8,
1091
- 11,
1092
- 14,
1093
- 14,
1094
- 6,
1095
- 14,
1096
- 6,
1097
- 9,
1098
- 12,
1099
- 9,
1100
- 12,
1101
- 5,
1102
- 15,
1103
- 8,
1104
- 8,
1105
- 5,
1106
- 12,
1107
- 9,
1108
- 12,
1109
- 5,
1110
- 14,
1111
- 6,
1112
- 8,
1113
- 13,
1114
- 6,
1115
- 5,
1116
- 15,
1117
- 13,
1118
- 11,
1119
- 11
1120
- ];
1121
- var KL = [0, 1518500249, 1859775393, 2400959708, 2840853838];
1122
- var KR = [1352829926, 1548603684, 1836072691, 2053994217, 0];
1123
- var f = (j, x, y, z) => {
1124
- if (j < 16) return x ^ y ^ z;
1125
- if (j < 32) return x & y | ~x & z;
1126
- if (j < 48) return (x | ~y) ^ z;
1127
- if (j < 64) return x & z | y & ~z;
1128
- return x ^ (y | ~z);
1129
- };
1130
- function ripemd160(msg) {
1131
- let h0 = 1732584193, h1 = 4023233417, h2 = 2562383102, h3 = 271733878, h4 = 3285377520;
1132
- const bitLen = msg.length * 8;
1133
- const withOne = msg.length + 1;
1134
- const padded = new Uint8Array(Math.ceil((withOne + 8) / 64) * 64);
1135
- padded.set(msg, 0);
1136
- padded[msg.length] = 128;
1137
- const dv = new DataView(padded.buffer);
1138
- dv.setUint32(padded.length - 8, bitLen >>> 0, true);
1139
- dv.setUint32(padded.length - 4, Math.floor(bitLen / 2 ** 32), true);
1140
- const x = new Uint32Array(16);
1141
- for (let off = 0; off < padded.length; off += 64) {
1142
- for (let i = 0; i < 16; i++) x[i] = dv.getUint32(off + i * 4, true);
1143
- let al = h0, bl = h1, cl = h2, dl = h3, el = h4;
1144
- let ar = h0, br = h1, cr = h2, dr = h3, er = h4;
1145
- for (let j = 0; j < 80; j++) {
1146
- const round = Math.floor(j / 16);
1147
- let t2 = al + f(j, bl, cl, dl) + x[ZL[j]] + KL[round] | 0;
1148
- t2 = rol(t2, SL[j]) + el | 0;
1149
- al = el;
1150
- el = dl;
1151
- dl = rol(cl, 10);
1152
- cl = bl;
1153
- bl = t2;
1154
- t2 = ar + f(79 - j, br, cr, dr) + x[ZR[j]] + KR[round] | 0;
1155
- t2 = rol(t2, SR[j]) + er | 0;
1156
- ar = er;
1157
- er = dr;
1158
- dr = rol(cr, 10);
1159
- cr = br;
1160
- br = t2;
1161
- }
1162
- const t = h1 + cl + dr | 0;
1163
- h1 = h2 + dl + er | 0;
1164
- h2 = h3 + el + ar | 0;
1165
- h3 = h4 + al + br | 0;
1166
- h4 = h0 + bl + cr | 0;
1167
- h0 = t;
1168
- }
1169
- const out = new Uint8Array(20);
1170
- const odv = new DataView(out.buffer);
1171
- odv.setUint32(0, h0, true);
1172
- odv.setUint32(4, h1, true);
1173
- odv.setUint32(8, h2, true);
1174
- odv.setUint32(12, h3, true);
1175
- odv.setUint32(16, h4, true);
1176
- return out;
1177
- }
1178
- var MAX_RESULT_LENGTH = 4096;
1179
- var MAX_MSG_LENGTH = 4096;
1180
- function concatBytes(a, b) {
1181
- const out = new Uint8Array(a.length + b.length);
1182
- out.set(a, 0);
1183
- out.set(b, a.length);
1184
- return out;
1185
- }
1186
- var Op = class _Op {
1187
- static MAX_RESULT_LENGTH = MAX_RESULT_LENGTH;
1188
- static MAX_MSG_LENGTH = MAX_MSG_LENGTH;
1189
- /** Deserializa una operación leyendo su tag y despachando a la factoría correcta. */
1190
- static deserialize(ctx) {
1191
- return _Op.deserializeFromTag(ctx, ctx.readByte());
1192
- }
1193
- /** Igual que `deserialize`, pero con el tag ya leído del stream (lo usa el árbol Timestamp). */
1194
- static deserializeFromTag(ctx, tag) {
1195
- const factory = OP_BY_TAG.get(tag);
1196
- if (factory === void 0) {
1197
- throw new UnknownOperationError(`unknown operation tag 0x${tag.toString(16).padStart(2, "0")}`);
1198
- }
1199
- return factory(ctx);
1200
- }
1201
- get maxMsgLength() {
1202
- return MAX_MSG_LENGTH;
1203
- }
1204
- checkMsg(msg) {
1205
- if (msg.length > this.maxMsgLength) {
1206
- throw new MessageTooLongError(`message length ${msg.length} exceeds ${this.maxMsgLength}`);
1207
- }
1208
- }
1209
- checkResult(result) {
1210
- if (result.length > MAX_RESULT_LENGTH) {
1211
- throw new ResultTooLongError(`result length ${result.length} exceeds ${MAX_RESULT_LENGTH}`);
1212
- }
1213
- return result;
1214
- }
1215
- };
1216
- var OpBinary = class extends Op {
1217
- arg;
1218
- constructor(arg) {
1219
- super();
1220
- if (!(arg instanceof Uint8Array)) {
1221
- throw new TypeError("OpBinary arg must be a Uint8Array");
1222
- }
1223
- this.arg = arg.slice();
1224
- }
1225
- serialize(ctx) {
1226
- ctx.writeByte(this.tag);
1227
- ctx.writeVarbytes(this.arg);
1228
- }
1229
- };
1230
- var OpAppend = class _OpAppend extends OpBinary {
1231
- static TAG = 240;
1232
- tag = _OpAppend.TAG;
1233
- tagName = "append";
1234
- call(msg) {
1235
- this.checkMsg(msg);
1236
- return this.checkResult(concatBytes(msg, this.arg));
1237
- }
1238
- equals(other) {
1239
- return other instanceof _OpAppend && bytesEqual(this.arg, other.arg);
1240
- }
1241
- };
1242
- var OpPrepend = class _OpPrepend extends OpBinary {
1243
- static TAG = 241;
1244
- tag = _OpPrepend.TAG;
1245
- tagName = "prepend";
1246
- call(msg) {
1247
- this.checkMsg(msg);
1248
- return this.checkResult(concatBytes(this.arg, msg));
1249
- }
1250
- equals(other) {
1251
- return other instanceof _OpPrepend && bytesEqual(this.arg, other.arg);
1252
- }
1253
- };
1254
- var OpUnary = class extends Op {
1255
- serialize(ctx) {
1256
- ctx.writeByte(this.tag);
1257
- }
1258
- };
1259
- var OpReverse = class _OpReverse extends OpUnary {
1260
- static TAG = 242;
1261
- tag = _OpReverse.TAG;
1262
- tagName = "reverse";
1263
- call(msg) {
1264
- this.checkMsg(msg);
1265
- const r = new Uint8Array(msg.length);
1266
- for (let i = 0; i < msg.length; i++) r[i] = msg[msg.length - 1 - i];
1267
- return this.checkResult(r);
1268
- }
1269
- equals(other) {
1270
- return other instanceof _OpReverse;
1271
- }
1272
- };
1273
- var OpHexlify = class _OpHexlify extends OpUnary {
1274
- static TAG = 243;
1275
- tag = _OpHexlify.TAG;
1276
- tagName = "hexlify";
1277
- // El resultado mide el doble que el mensaje; el límite de mensaje es la mitad.
1278
- get maxMsgLength() {
1279
- return MAX_RESULT_LENGTH / 2;
1280
- }
1281
- call(msg) {
1282
- this.checkMsg(msg);
1283
- return this.checkResult(textToBytes(bytesToHex(msg)));
1284
- }
1285
- equals(other) {
1286
- return other instanceof _OpHexlify;
1287
- }
1288
- };
1289
- var CryptOp = class extends OpUnary {
1290
- call(msg) {
1291
- this.checkMsg(msg);
1292
- return this.hash(msg);
1293
- }
1294
- /**
1295
- * Hashea el contenido COMPLETO de un fichero (longitud arbitraria) con el algoritmo
1296
- * de esta operación. A diferencia de `call`, NO aplica el límite `MAX_MSG_LENGTH`:
1297
- * `call` transforma digests dentro del árbol de prueba (≤ 4096 bytes), mientras que
1298
- * `hashFile` recibe el contenido íntegro del fichero a sellar, que puede ser de
1299
- * cualquier tamaño. Lo usa `DetachedTimestampFile.fromBytes`.
1300
- */
1301
- hashFile(data) {
1302
- if (!(data instanceof Uint8Array)) {
1303
- throw new TypeError("hashFile expects a Uint8Array");
1304
- }
1305
- return this.hash(data);
1306
- }
1307
- };
1308
- var OpSHA1 = class _OpSHA1 extends CryptOp {
1309
- static TAG = 2;
1310
- tag = _OpSHA1.TAG;
1311
- tagName = "sha1";
1312
- digestLength = 20;
1313
- hash(msg) {
1314
- return sha1(msg);
1315
- }
1316
- equals(other) {
1317
- return other instanceof _OpSHA1;
1318
- }
1319
- };
1320
- var OpRIPEMD160 = class _OpRIPEMD160 extends CryptOp {
1321
- static TAG = 3;
1322
- tag = _OpRIPEMD160.TAG;
1323
- tagName = "ripemd160";
1324
- digestLength = 20;
1325
- hash(msg) {
1326
- return ripemd160(msg);
1327
- }
1328
- equals(other) {
1329
- return other instanceof _OpRIPEMD160;
1330
- }
1331
- };
1332
- var OpSHA256 = class _OpSHA256 extends CryptOp {
1333
- static TAG = 8;
1334
- tag = _OpSHA256.TAG;
1335
- tagName = "sha256";
1336
- digestLength = 32;
1337
- hash(msg) {
1338
- return sha256(msg);
1339
- }
1340
- equals(other) {
1341
- return other instanceof _OpSHA256;
1342
- }
1343
- };
1344
- var unary = (ctor) => () => new ctor();
1345
- var binary = (ctor) => (ctx) => new ctor(ctx.readVarbytes(MAX_RESULT_LENGTH, 1));
1346
- function buildTagTable(entries) {
1347
- const map = /* @__PURE__ */ new Map();
1348
- for (const [tag, factory] of entries) {
1349
- if (map.has(tag)) {
1350
- throw new Error(`duplicate operation tag 0x${tag.toString(16)}`);
1351
- }
1352
- map.set(tag, factory);
1353
- }
1354
- return map;
1355
- }
1356
- var OP_BY_TAG = buildTagTable([
1357
- [OpAppend.TAG, binary(OpAppend)],
1358
- [OpPrepend.TAG, binary(OpPrepend)],
1359
- [OpReverse.TAG, unary(OpReverse)],
1360
- [OpHexlify.TAG, unary(OpHexlify)],
1361
- [OpSHA1.TAG, unary(OpSHA1)],
1362
- [OpRIPEMD160.TAG, unary(OpRIPEMD160)],
1363
- [OpSHA256.TAG, unary(OpSHA256)]
1364
- ]);
1365
- var TAG_SIZE = 8;
1366
- var MAX_PAYLOAD_SIZE = 8192;
1367
- var MAX_URI_LENGTH = 1e3;
1368
- var ALLOWED_URI_CHARS = new Set(
1369
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._/:"
1370
- );
1371
- var PENDING_TAG = new Uint8Array([131, 223, 227, 13, 46, 249, 12, 142]);
1372
- var BITCOIN_TAG = new Uint8Array([5, 136, 150, 13, 115, 215, 25, 1]);
1373
- var LITECOIN_TAG = new Uint8Array([6, 134, 154, 13, 115, 215, 27, 69]);
1374
- function decodeAndValidateUri(bytes) {
1375
- if (bytes.length === 0) {
1376
- throw new InvalidUriError("pending attestation URI is empty");
1377
- }
1378
- if (bytes.length > MAX_URI_LENGTH) {
1379
- throw new InvalidUriError(`pending attestation URI exceeds ${MAX_URI_LENGTH} bytes`);
1380
- }
1381
- let uri = "";
1382
- for (let i = 0; i < bytes.length; i++) {
1383
- const byte = bytes[i];
1384
- const char = String.fromCharCode(byte);
1385
- if (!ALLOWED_URI_CHARS.has(char)) {
1386
- throw new InvalidUriError(
1387
- `pending attestation URI contains invalid byte 0x${byte.toString(16).padStart(2, "0")}`
1388
- );
1389
- }
1390
- uri += char;
1391
- }
1392
- return uri;
1393
- }
1394
- function deserializeAttestation(ctx) {
1395
- const tag = ctx.read(TAG_SIZE).slice();
1396
- const payload = ctx.readVarbytes(MAX_PAYLOAD_SIZE);
1397
- const payloadCtx = new StreamDeserializationContext(payload);
1398
- let attestation;
1399
- if (bytesEqual(tag, PENDING_TAG)) {
1400
- const uriBytes = payloadCtx.readVarbytes(MAX_URI_LENGTH).slice();
1401
- attestation = { kind: "pending", tag, uri: decodeAndValidateUri(uriBytes), uriBytes };
1402
- } else if (bytesEqual(tag, BITCOIN_TAG)) {
1403
- attestation = { kind: "bitcoin", tag, height: payloadCtx.readVaruint() };
1404
- } else if (bytesEqual(tag, LITECOIN_TAG)) {
1405
- attestation = { kind: "litecoin", tag, height: payloadCtx.readVaruint() };
1406
- } else {
1407
- return { kind: "unknown", tag, payload: payload.slice() };
1408
- }
1409
- payloadCtx.assertEof();
1410
- return attestation;
1411
- }
1412
- function serializePayload(ctx, att) {
1413
- switch (att.kind) {
1414
- case "pending":
1415
- ctx.writeVarbytes(att.uriBytes);
1416
- return;
1417
- case "bitcoin":
1418
- case "litecoin":
1419
- ctx.writeVaruint(att.height);
1420
- return;
1421
- case "unknown":
1422
- ctx.writeBytes(att.payload);
1423
- return;
1424
- }
1425
- }
1426
- function serializeAttestation(ctx, att) {
1427
- ctx.writeBytes(att.tag);
1428
- const payloadCtx = new StreamSerializationContext();
1429
- serializePayload(payloadCtx, att);
1430
- ctx.writeVarbytes(payloadCtx.getOutput());
1431
- }
1432
- function compareAttestations(a, b) {
1433
- const deltaTag = compareBytes(a.tag, b.tag);
1434
- if (deltaTag !== 0) {
1435
- return deltaTag;
1436
- }
1437
- switch (a.kind) {
1438
- case "pending":
1439
- return compareBytes(a.uriBytes, b.uriBytes);
1440
- case "bitcoin":
1441
- case "litecoin":
1442
- return a.height - b.height;
1443
- case "unknown":
1444
- return compareBytes(a.payload, b.payload);
1445
- }
1446
- }
1447
- function attestationsEqual(a, b) {
1448
- if (a.kind !== b.kind || !bytesEqual(a.tag, b.tag)) {
1449
- return false;
1450
- }
1451
- switch (a.kind) {
1452
- case "pending":
1453
- return bytesEqual(a.uriBytes, b.uriBytes);
1454
- case "bitcoin":
1455
- case "litecoin":
1456
- return a.height === b.height;
1457
- case "unknown":
1458
- return bytesEqual(a.payload, b.payload);
1459
- }
1460
- }
1461
- var MERKLEROOT_RE = /^[0-9a-fA-F]{64}$/;
1462
- function verifyAgainstBlockheader(digest, block) {
1463
- if (digest.length !== 32) {
1464
- throw new VerificationError(`expected digest of 32 bytes; got ${digest.length}`);
1465
- }
1466
- if (typeof block.merkleroot !== "string" || !MERKLEROOT_RE.test(block.merkleroot)) {
1467
- throw new VerificationError("block merkleroot is not a 64-char hex string");
1468
- }
1469
- if (!Number.isInteger(block.time) || block.time <= 0) {
1470
- throw new VerificationError("block time is not a positive integer");
1471
- }
1472
- if (!bytesEqual(digest, hexToBytes(block.merkleroot))) {
1473
- throw new VerificationError("digest does not match block merkleroot");
1474
- }
1475
- return block.time;
1476
- }
1477
- var MAX_TREE_DEPTH = 256;
1478
- function opToBytes(op) {
1479
- const ctx = new StreamSerializationContext();
1480
- op.serialize(ctx);
1481
- return ctx.getOutput();
1482
- }
1483
- function opKey(op) {
1484
- return bytesToHex(opToBytes(op));
1485
- }
1486
- var Timestamp = class _Timestamp {
1487
- /** Digest de este nodo (copia defensiva, no comparte memoria con la entrada). */
1488
- msg;
1489
- /** Sellos directos sobre `msg`. El cliente puede añadir con `.push()`. */
1490
- attestations = [];
1491
- /** Ramas indexadas por la serialización canónica (hex) de su op. */
1492
- #ops = /* @__PURE__ */ new Map();
1493
- constructor(msg) {
1494
- if (!(msg instanceof Uint8Array)) {
1495
- throw new TypeError("Timestamp msg must be a Uint8Array");
1496
- }
1497
- if (msg.length > Op.MAX_MSG_LENGTH) {
1498
- throw new TypeError(`Timestamp msg length ${msg.length} exceeds ${Op.MAX_MSG_LENGTH}`);
1499
- }
1500
- this.msg = msg.slice();
1501
- }
1502
- /** El digest de este nodo (copia: mutar el resultado no afecta al árbol). */
1503
- getDigest() {
1504
- return this.msg.slice();
1505
- }
1506
- /** Las ramas (op + sub-timestamp) de este nodo, como array de solo lectura. */
1507
- get branches() {
1508
- return [...this.#ops.values()];
1509
- }
1510
- /**
1511
- * Deserializa un Timestamp. El formato no incluye el mensaje sobre el que opera,
1512
- * así que hay que aportarlo (`initialMsg`) para recalcular los resultados de las ops.
1513
- * @param depth profundidad actual; protege contra árboles maliciosamente profundos.
1514
- */
1515
- static deserialize(ctx, initialMsg, depth = 0) {
1516
- if (depth > MAX_TREE_DEPTH) {
1517
- throw new OversizedDataError(`timestamp tree exceeds max depth ${MAX_TREE_DEPTH}`);
1518
- }
1519
- const self = new _Timestamp(initialMsg);
1520
- let tag = ctx.readByte();
1521
- while (tag === 255) {
1522
- self.#deserializeElement(ctx, ctx.readByte(), depth);
1523
- tag = ctx.readByte();
1524
- }
1525
- self.#deserializeElement(ctx, tag, depth);
1526
- return self;
1527
- }
1528
- #deserializeElement(ctx, tag, depth) {
1529
- if (tag === 0) {
1530
- this.attestations.push(deserializeAttestation(ctx));
1531
- return;
1532
- }
1533
- const op = Op.deserializeFromTag(ctx, tag);
1534
- let result;
1535
- try {
1536
- result = op.call(this.msg);
1537
- } catch (err) {
1538
- throw new DeserializationError(
1539
- `operation failed during deserialization: ${err.message}`
1540
- );
1541
- }
1542
- const stamp = _Timestamp.deserialize(ctx, result, depth + 1);
1543
- this.#ops.set(opKey(op), { op, stamp });
1544
- }
1545
- /** Serializa este nodo en orden canónico (determinista byte-a-byte). */
1546
- serialize(ctx) {
1547
- const attestations = [...this.attestations].sort(compareAttestations);
1548
- const branches = [...this.#ops.values()].sort(
1549
- (a, b) => compareBytes(opToBytes(a.op), opToBytes(b.op))
1550
- );
1551
- const total = attestations.length + branches.length;
1552
- if (total === 0) {
1553
- throw new EmptyTimestampError("an empty timestamp cannot be serialized");
1554
- }
1555
- let index = 0;
1556
- for (const attestation of attestations) {
1557
- if (index < total - 1) ctx.writeByte(255);
1558
- ctx.writeByte(0);
1559
- serializeAttestation(ctx, attestation);
1560
- index++;
1561
- }
1562
- for (const { op, stamp } of branches) {
1563
- if (index < total - 1) ctx.writeByte(255);
1564
- op.serialize(ctx);
1565
- stamp.serialize(ctx);
1566
- index++;
1567
- }
1568
- }
1569
- /**
1570
- * Añade una op a este nodo y devuelve el sub-timestamp de su resultado.
1571
- * Si la op (por contenido) ya existe, devuelve la rama existente.
1572
- */
1573
- add(op) {
1574
- const key = opKey(op);
1575
- const existing = this.#ops.get(key);
1576
- if (existing !== void 0) {
1577
- return existing.stamp;
1578
- }
1579
- const stamp = new _Timestamp(op.call(this.msg));
1580
- this.#ops.set(key, { op, stamp });
1581
- return stamp;
1582
- }
1583
- /**
1584
- * Vincula `op` a un sub-timestamp YA EXISTENTE, compartiendo el objeto (no crea uno nuevo).
1585
- * A diferencia de `add`, hace que esta rama apunte al mismo `Timestamp` que otra rama
1586
- * (de otro nodo) ya construyó, de modo que las attestations añadidas más arriba sean
1587
- * alcanzables desde ambos caminos. Lo usa el árbol Merkle para el cross-link izquierda/derecha.
1588
- * Falla (fail-closed) si `stamp` no es un Timestamp o si `op.call(this.msg)` no coincide con `stamp.msg`.
1589
- */
1590
- addExisting(op, stamp) {
1591
- if (!(stamp instanceof _Timestamp)) {
1592
- throw new TypeError("addExisting requires a Timestamp");
1593
- }
1594
- if (!bytesEqual(op.call(this.msg), stamp.msg)) {
1595
- throw new MergeError("operation result does not match the existing timestamp message");
1596
- }
1597
- this.#ops.set(opKey(op), { op, stamp });
1598
- return stamp;
1599
- }
1600
- /** Incorpora las attestations y ramas de `other` (mismo `msg`) en este timestamp. */
1601
- merge(other) {
1602
- if (!(other instanceof _Timestamp)) {
1603
- throw new MergeError("can only merge Timestamps together");
1604
- }
1605
- if (!bytesEqual(this.msg, other.msg)) {
1606
- throw new MergeError("cannot merge timestamps for different messages");
1607
- }
1608
- for (const attestation of other.attestations) {
1609
- if (!this.attestations.some((existing) => attestationsEqual(existing, attestation))) {
1610
- this.attestations.push(attestation);
1611
- }
1612
- }
1613
- for (const { op, stamp } of other.#ops.values()) {
1614
- const key = opKey(op);
1615
- let branch = this.#ops.get(key);
1616
- if (branch === void 0) {
1617
- branch = { op, stamp: new _Timestamp(op.call(this.msg)) };
1618
- this.#ops.set(key, branch);
1619
- }
1620
- branch.stamp.merge(stamp);
1621
- }
1622
- }
1623
- /** Todas las attestations del árbol con el msg de su nodo (sin pérdida de datos). */
1624
- allAttestations() {
1625
- const result = [];
1626
- for (const attestation of this.attestations) {
1627
- result.push({ msg: this.msg.slice(), attestation });
1628
- }
1629
- for (const { stamp } of this.#ops.values()) {
1630
- result.push(...stamp.allAttestations());
1631
- }
1632
- return result;
1633
- }
1634
- /** Todas las attestations del árbol (sin el msg asociado). */
1635
- getAttestations() {
1636
- return this.allAttestations().map((entry) => entry.attestation);
1637
- }
1638
- /** Verdadero si el árbol contiene una attestation verificable localmente (Bitcoin/Litecoin). */
1639
- isTimestampComplete() {
1640
- return this.allAttestations().some(
1641
- ({ attestation }) => attestation.kind === "bitcoin" || attestation.kind === "litecoin"
1642
- );
1643
- }
1644
- /** Sub-timestamps que tienen attestations directas. */
1645
- directlyVerified() {
1646
- if (this.attestations.length > 0) {
1647
- return [this];
1648
- }
1649
- const result = [];
1650
- for (const { stamp } of this.#ops.values()) {
1651
- result.push(...stamp.directlyVerified());
1652
- }
1653
- return result;
1654
- }
1655
- /** Los mensajes de las hojas del árbol (nodos sin ops). */
1656
- allTips() {
1657
- if (this.#ops.size === 0) {
1658
- return [this.msg.slice()];
1659
- }
1660
- const result = [];
1661
- for (const { stamp } of this.#ops.values()) {
1662
- result.push(...stamp.allTips());
1663
- }
1664
- return result;
1665
- }
1666
- /** Igualdad estructural recursiva con otro timestamp. */
1667
- equals(other) {
1668
- if (!(other instanceof _Timestamp)) {
1669
- return false;
1670
- }
1671
- if (!bytesEqual(this.msg, other.msg)) {
1672
- return false;
1673
- }
1674
- if (this.attestations.length !== other.attestations.length) {
1675
- return false;
1676
- }
1677
- const ours = [...this.attestations].sort(compareAttestations);
1678
- const theirs = [...other.attestations].sort(compareAttestations);
1679
- for (let i = 0; i < ours.length; i++) {
1680
- if (!attestationsEqual(ours[i], theirs[i])) {
1681
- return false;
1682
- }
1683
- }
1684
- if (this.#ops.size !== other.#ops.size) {
1685
- return false;
1686
- }
1687
- for (const [key, branch] of this.#ops) {
1688
- const otherBranch = other.#ops.get(key);
1689
- if (otherBranch === void 0) {
1690
- return false;
1691
- }
1692
- if (!branch.stamp.equals(otherBranch.stamp)) {
1693
- return false;
1694
- }
1695
- }
1696
- return true;
1697
- }
1698
- };
1699
- function catSha256(left, right) {
1700
- if (!(left instanceof Timestamp) || !(right instanceof Timestamp)) {
1701
- throw new TypeError("catSha256 requires two Timestamps");
1702
- }
1703
- const concat = right.add(new OpPrepend(left.msg));
1704
- left.addExisting(new OpAppend(right.msg), concat);
1705
- return concat.add(new OpSHA256());
1706
- }
1707
- function makeMerkleTree(timestamps) {
1708
- if (timestamps.length === 0) {
1709
- throw new EmptyMerkleTreeError("makeMerkleTree requires at least one timestamp");
1710
- }
1711
- for (const stamp of timestamps) {
1712
- if (!(stamp instanceof Timestamp)) {
1713
- throw new TypeError("makeMerkleTree requires an array of Timestamps");
1714
- }
1715
- }
1716
- let round = [...timestamps];
1717
- while (round.length > 1) {
1718
- const next = [];
1719
- for (let i = 0; i < round.length; i += 2) {
1720
- if (i + 1 < round.length) {
1721
- next.push(catSha256(round[i], round[i + 1]));
1722
- } else {
1723
- next.push(round[i]);
1724
- }
1725
- }
1726
- round = next;
1727
- }
1728
- return round[0];
1729
- }
1730
- var HEADER_MAGIC = new Uint8Array([
1731
- 0,
1732
- 79,
1733
- 112,
1734
- 101,
1735
- 110,
1736
- 84,
1737
- 105,
1738
- 109,
1739
- 101,
1740
- 115,
1741
- 116,
1742
- 97,
1743
- 109,
1744
- 112,
1745
- 115,
1746
- 0,
1747
- 0,
1748
- 80,
1749
- 114,
1750
- 111,
1751
- 111,
1752
- 102,
1753
- 0,
1754
- 191,
1755
- 137,
1756
- 226,
1757
- 232,
1758
- 132,
1759
- 232,
1760
- 146,
1761
- 148
1762
- ]);
1763
- var MAJOR_VERSION = 1;
1764
- var DetachedTimestampFile = class _DetachedTimestampFile {
1765
- fileHashOp;
1766
- timestamp;
1767
- constructor(fileHashOp, timestamp) {
1768
- if (!(fileHashOp instanceof CryptOp)) {
1769
- throw new TypeError("DetachedTimestampFile: fileHashOp must be a CryptOp");
1770
- }
1771
- if (!(timestamp instanceof Timestamp)) {
1772
- throw new TypeError("DetachedTimestampFile: timestamp must be a Timestamp");
1773
- }
1774
- if (timestamp.msg.length !== fileHashOp.digestLength) {
1775
- throw new TypeError(
1776
- `DetachedTimestampFile: timestamp message length ${timestamp.msg.length} does not match ${fileHashOp.tagName} digest length ${fileHashOp.digestLength}`
1777
- );
1778
- }
1779
- this.fileHashOp = fileHashOp;
1780
- this.timestamp = timestamp;
1781
- }
1782
- /** Digest del fichero sellado (copia defensiva: mutarla no afecta al objeto). */
1783
- fileDigest() {
1784
- return this.timestamp.getDigest();
1785
- }
1786
- /** Escribe el fichero `.ots` en el contexto: magic → versión → op → digest → árbol. */
1787
- serialize(ctx) {
1788
- ctx.writeBytes(HEADER_MAGIC);
1789
- ctx.writeVaruint(MAJOR_VERSION);
1790
- this.fileHashOp.serialize(ctx);
1791
- ctx.writeBytes(this.timestamp.msg);
1792
- this.timestamp.serialize(ctx);
1793
- }
1794
- /** Serializa el fichero `.ots` completo a bytes. */
1795
- serializeToBytes() {
1796
- const ctx = new StreamSerializationContext();
1797
- this.serialize(ctx);
1798
- return ctx.getOutput();
1799
- }
1800
- /**
1801
- * Lee un fichero `.ots` desde bytes. Único tipo de entrada: `Uint8Array` (fail-closed;
1802
- * elimina los 4 tipos del original y el bug `Array.from(ArrayBuffer) → []`).
1803
- */
1804
- static deserialize(input) {
1805
- if (!(input instanceof Uint8Array)) {
1806
- throw new TypeError("DetachedTimestampFile.deserialize expects a Uint8Array");
1807
- }
1808
- const ctx = new StreamDeserializationContext(input);
1809
- ctx.assertMagic(HEADER_MAGIC);
1810
- const major = ctx.readVaruint();
1811
- if (major !== MAJOR_VERSION) {
1812
- throw new UnsupportedVersionError(`unsupported .ots major version ${major}`);
1813
- }
1814
- const op = Op.deserialize(ctx);
1815
- if (!(op instanceof CryptOp)) {
1816
- throw new DeserializationError("file hash operation must be a cryptographic hash");
1817
- }
1818
- const fileHash = ctx.read(op.digestLength);
1819
- const timestamp = Timestamp.deserialize(ctx, fileHash);
1820
- ctx.assertEof();
1821
- return new _DetachedTimestampFile(op, timestamp);
1822
- }
1823
- /** Crea un `.ots` nuevo hasheando el contenido completo de un fichero. */
1824
- static fromBytes(fileHashOp, fileContent) {
1825
- if (!(fileHashOp instanceof CryptOp)) {
1826
- throw new TypeError("DetachedTimestampFile.fromBytes: fileHashOp must be a CryptOp");
1827
- }
1828
- const digest = fileHashOp.hashFile(fileContent);
1829
- return new _DetachedTimestampFile(fileHashOp, new Timestamp(digest));
1830
- }
1831
- /** Crea un `.ots` nuevo a partir de un digest ya calculado del fichero. */
1832
- static fromHash(fileHashOp, fileDigest) {
1833
- return new _DetachedTimestampFile(fileHashOp, new Timestamp(fileDigest));
1834
- }
1835
- /** Igualdad estructural con otro fichero `.ots`. */
1836
- equals(other) {
1837
- return other instanceof _DetachedTimestampFile && this.fileHashOp.equals(other.fileHashOp) && this.timestamp.equals(other.timestamp);
1838
- }
1839
- };
432
+ // src/core/orchestration.ts
433
+ import {
434
+ DetachedTimestampFile,
435
+ OpSHA256,
436
+ OpAppend,
437
+ makeMerkleTree
438
+ } from "@otskit/core";
1840
439
 
1841
440
  // src/network/calendar.ts
441
+ import {
442
+ Timestamp,
443
+ StreamDeserializationContext,
444
+ bytesToHex,
445
+ TRUSTED_CALENDAR_WHITELIST_PATTERNS,
446
+ DEFAULT_AGGREGATOR_URLS
447
+ } from "@otskit/core";
1842
448
  var MAX_CALENDAR_RESPONSE_SIZE = 1e4;
1843
449
  function assertCommitment(bytes) {
1844
450
  if (!(bytes instanceof Uint8Array)) {
@@ -1864,7 +470,7 @@ var CalendarClient = class {
1864
470
  url;
1865
471
  networkLayer;
1866
472
  logger;
1867
- /** Envía un digest al calendario y devuelve el Timestamp que lo commit-ea. */
473
+ /** Envia un digest al calendario y devuelve el Timestamp que lo commit-ea. */
1868
474
  async submit(digest, signal) {
1869
475
  assertCommitment(digest);
1870
476
  this.logger?.debug(`Submitting digest to ${this.url}/digest`);
@@ -1875,7 +481,7 @@ var CalendarClient = class {
1875
481
  );
1876
482
  return this.#parseTimestamp(response.data, digest);
1877
483
  }
1878
- /** Pregunta al calendario si tiene un Timestamp más completo para `commitment` (upgrade). */
484
+ /** Pregunta al calendario si tiene un Timestamp mas completo para `commitment` (upgrade). */
1879
485
  async getTimestamp(commitment, signal) {
1880
486
  assertCommitment(commitment);
1881
487
  const path = `/timestamp/${bytesToHex(commitment)}`;
@@ -1910,56 +516,79 @@ var CalendarClient = class {
1910
516
  return timestamp;
1911
517
  }
1912
518
  };
1913
- function wildcardToRegExp(pattern) {
1914
- const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, "[^/]*");
1915
- return new RegExp(`^${escaped}$`, "i");
519
+ function parseWhitelistPattern(raw) {
520
+ let parsed;
521
+ try {
522
+ parsed = new URL(raw);
523
+ } catch {
524
+ return void 0;
525
+ }
526
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return void 0;
527
+ const hostname = parsed.hostname.toLowerCase();
528
+ const wildcardSuffix = hostname.startsWith("*.") ? hostname.slice(2) : void 0;
529
+ if (hostname.includes("*") && wildcardSuffix === void 0) return void 0;
530
+ if (wildcardSuffix !== void 0 && (wildcardSuffix.length === 0 || wildcardSuffix.includes("*"))) return void 0;
531
+ return {
532
+ protocol: parsed.protocol,
533
+ hostname,
534
+ port: parsed.port,
535
+ pathname: parsed.pathname,
536
+ wildcardSuffix
537
+ };
538
+ }
539
+ function hostnameMatchesPattern(hostname, pattern) {
540
+ if (pattern.wildcardSuffix === void 0) return hostname === pattern.hostname;
541
+ if (!hostname.endsWith("." + pattern.wildcardSuffix)) return false;
542
+ const label = hostname.slice(0, -pattern.wildcardSuffix.length - 1);
543
+ return label.length > 0 && !label.includes(".");
1916
544
  }
1917
545
  var UrlWhitelist = class {
1918
- #patterns = /* @__PURE__ */ new Set();
546
+ #patterns = /* @__PURE__ */ new Map();
1919
547
  constructor(urls) {
1920
548
  if (urls) {
1921
549
  for (const u of urls) this.add(u);
1922
550
  }
1923
551
  }
1924
- /** Añade un patrón; si no trae esquema, se añaden las variantes http y https. */
552
+ /** Anade un patron; si no trae esquema, se anaden las variantes http y https. */
1925
553
  add(url) {
1926
554
  if (typeof url !== "string") {
1927
555
  throw new TypeError("UrlWhitelist: URL must be a string");
1928
556
  }
1929
557
  if (url.startsWith("http://") || url.startsWith("https://")) {
1930
- this.#patterns.add(url);
558
+ const pattern = parseWhitelistPattern(url);
559
+ if (pattern !== void 0) this.#patterns.set(url, pattern);
1931
560
  } else {
1932
- this.#patterns.add("http://" + url);
1933
- this.#patterns.add("https://" + url);
561
+ this.add("http://" + url);
562
+ this.add("https://" + url);
1934
563
  }
1935
564
  }
1936
- /** Verdadero si `url` casa con algún patrón de la whitelist. */
565
+ /** Verdadero si `url` casa con algun patron de la whitelist. */
1937
566
  contains(url) {
1938
- for (const pattern of this.#patterns) {
1939
- if (wildcardToRegExp(pattern).test(url)) return true;
567
+ let parsed;
568
+ try {
569
+ parsed = new URL(url);
570
+ } catch {
571
+ return false;
572
+ }
573
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
574
+ if (parsed.search !== "" || parsed.hash !== "") return false;
575
+ const hostname = parsed.hostname.toLowerCase();
576
+ for (const pattern of this.#patterns.values()) {
577
+ if (parsed.protocol !== pattern.protocol || parsed.port !== pattern.port) continue;
578
+ if (parsed.pathname !== pattern.pathname) continue;
579
+ if (hostnameMatchesPattern(hostname, pattern)) return true;
1940
580
  }
1941
581
  return false;
1942
582
  }
1943
583
  toString() {
1944
- return `UrlWhitelist([${[...this.#patterns].join(", ")}])`;
584
+ return `UrlWhitelist([${[...this.#patterns.keys()].join(", ")}])`;
1945
585
  }
1946
586
  };
1947
- var DEFAULT_CALENDAR_WHITELIST = new UrlWhitelist([
1948
- "https://*.calendar.opentimestamps.org",
1949
- // Peter Todd
1950
- "https://*.calendar.eternitywall.com",
1951
- // Eternity Wall
1952
- "https://*.calendar.catallaxy.com"
1953
- // Catallaxy
1954
- ]);
1955
- var DEFAULT_AGGREGATORS = [
1956
- "https://a.pool.opentimestamps.org",
1957
- "https://b.pool.opentimestamps.org",
1958
- "https://a.pool.eternitywall.com",
1959
- "https://ots.btc.catallaxy.com"
1960
- ];
587
+ var DEFAULT_CALENDAR_WHITELIST = new UrlWhitelist([...TRUSTED_CALENDAR_WHITELIST_PATTERNS]);
588
+ var DEFAULT_AGGREGATORS = [...DEFAULT_AGGREGATOR_URLS];
1961
589
 
1962
590
  // src/network/esplora.ts
591
+ import { verifyAgainstBlockheader, VerificationError } from "@otskit/core";
1963
592
  var PUBLIC_ESPLORA_URL = "https://blockstream.info/api";
1964
593
  var MAX_ESPLORA_RESPONSE_SIZE = 1e5;
1965
594
  var HEX64_RE = /^[0-9a-f]{64}$/i;
@@ -2039,7 +668,13 @@ var EsploraClient = class {
2039
668
  `esplora response of ${data.length} bytes exceeds limit ${MAX_ESPLORA_RESPONSE_SIZE}`
2040
669
  );
2041
670
  }
2042
- return new TextDecoder("utf-8", { fatal: false }).decode(data);
671
+ try {
672
+ return new TextDecoder("utf-8", { fatal: true }).decode(data);
673
+ } catch (cause) {
674
+ throw new EsploraResponseError("esplora response contains invalid UTF-8 bytes", {
675
+ cause: cause instanceof Error ? cause : void 0
676
+ });
677
+ }
2043
678
  }
2044
679
  };
2045
680
  async function verifyTimestampAttestation(digest, attestation, explorer, signal) {
@@ -2052,6 +687,106 @@ async function verifyTimestampAttestation(digest, attestation, explorer, signal)
2052
687
  }
2053
688
 
2054
689
  // src/core/orchestration.ts
690
+ import { timingSafeEqual } from "crypto";
691
+
692
+ // src/security/ssrf.ts
693
+ import { lookup } from "dns/promises";
694
+ import { isIP } from "net";
695
+ var BLOCKED_CIDRS_V4 = [
696
+ { network: 0, mask: 4278190080, label: "0.0.0.0/8 (This Network)" },
697
+ { network: 167772160, mask: 4278190080, label: "10.0.0.0/8 (RFC 1918)" },
698
+ { network: 2130706432, mask: 4278190080, label: "127.0.0.0/8 (Loopback)" },
699
+ { network: 2851995648, mask: 4294901760, label: "169.254.0.0/16 (Link-local/IMDS)" },
700
+ { network: 2886729728, mask: 4293918720, label: "172.16.0.0/12 (RFC 1918)" },
701
+ { network: 3232235520, mask: 4294901760, label: "192.168.0.0/16 (RFC 1918)" },
702
+ { network: 3323068416, mask: 4294836224, label: "198.18.0.0/15 (Benchmarking)" },
703
+ { network: 3758096384, mask: 4026531840, label: "224.0.0.0/4 (Multicast)" },
704
+ { network: 4026531840, mask: 4026531840, label: "240.0.0.0/4 (Reserved)" },
705
+ { network: 4294967295, mask: 4294967295, label: "255.255.255.255 (Broadcast)" }
706
+ ];
707
+ function ipv4ToUint32(ip) {
708
+ 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;
710
+ }
711
+ function assertNotPrivateIPv4(ip, calendarUrl) {
712
+ const n = ipv4ToUint32(ip);
713
+ for (const cidr of BLOCKED_CIDRS_V4) {
714
+ if ((n & cidr.mask) >>> 0 === cidr.network) {
715
+ throw new ValidationError(
716
+ `Calendar URL "${calendarUrl}" resolves to a private/reserved IPv4 address (${ip} \u2014 ${cidr.label}). Set allowPrivateCalendars: true to override.`
717
+ );
718
+ }
719
+ }
720
+ }
721
+ var BLOCKED_IPV6_PREFIXES = [
722
+ { prefix: "::1", label: "Loopback" },
723
+ { prefix: "::", label: "Unspecified" },
724
+ { prefix: "fc", label: "fc00::/7 (Unique Local)" },
725
+ { prefix: "fd", label: "fd00::/8 (Unique Local)" },
726
+ { prefix: "fe8", label: "fe80::/10 (Link-local)" },
727
+ { prefix: "fe9", label: "fe80::/10 (Link-local)" },
728
+ { prefix: "fea", label: "fe80::/10 (Link-local)" },
729
+ { prefix: "feb", label: "fe80::/10 (Link-local)" },
730
+ { prefix: "ff", label: "ff00::/8 (Multicast)" },
731
+ { prefix: "::ffff:", label: "IPv4-mapped IPv6" },
732
+ { prefix: "64:ff9b:", label: "64:ff9b::/96 (NAT64)" },
733
+ { prefix: "2001:db8", label: "2001:db8::/32 (Documentation)" }
734
+ ];
735
+ function assertNotPrivateIPv6(ip, calendarUrl) {
736
+ const lower = ip.toLowerCase();
737
+ for (const { prefix, label } of BLOCKED_IPV6_PREFIXES) {
738
+ if (lower === prefix || lower.startsWith(prefix)) {
739
+ throw new ValidationError(
740
+ `Calendar URL "${calendarUrl}" resolves to a private/reserved IPv6 address (${ip} \u2014 ${label}). Set allowPrivateCalendars: true to override.`
741
+ );
742
+ }
743
+ }
744
+ }
745
+ async function assertSafeCalendarUrl(url, options) {
746
+ let parsed;
747
+ try {
748
+ parsed = new URL(url);
749
+ } catch {
750
+ throw new ValidationError(`Calendar URL is not valid: "${url}"`);
751
+ }
752
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
753
+ throw new ValidationError(`Calendar URL must use http or https: "${url}"`);
754
+ }
755
+ if (parsed.username || parsed.password) {
756
+ throw new ValidationError(`Calendar URL must not contain embedded credentials: "${url}"`);
757
+ }
758
+ if (options.allowPrivate) return;
759
+ const hostname = parsed.hostname;
760
+ const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
761
+ const ipVersion = isIP(host);
762
+ if (ipVersion === 4) {
763
+ assertNotPrivateIPv4(host, url);
764
+ return;
765
+ }
766
+ if (ipVersion === 6) {
767
+ assertNotPrivateIPv6(host, url);
768
+ return;
769
+ }
770
+ let addresses;
771
+ try {
772
+ addresses = await lookup(hostname, { all: true });
773
+ } catch (err) {
774
+ const message = err instanceof Error ? err.message : String(err);
775
+ throw new ValidationError(
776
+ `Calendar URL hostname "${hostname}" could not be resolved: ${message}`
777
+ );
778
+ }
779
+ if (addresses.length === 0) {
780
+ throw new ValidationError(`Calendar URL hostname "${hostname}" resolved to no addresses`);
781
+ }
782
+ for (const { address, family } of addresses) {
783
+ if (family === 4) assertNotPrivateIPv4(address, url);
784
+ else if (family === 6) assertNotPrivateIPv6(address, url);
785
+ }
786
+ }
787
+
788
+ // src/core/orchestration.ts
789
+ var MAX_BITCOIN_ATTESTATIONS = 10;
2055
790
  function validateHash(hash) {
2056
791
  if (typeof hash === "string") {
2057
792
  const hex = hash.trim().toLowerCase();
@@ -2073,19 +808,12 @@ function secureNonce(n) {
2073
808
  globalThis.crypto.getRandomValues(bytes);
2074
809
  return bytes;
2075
810
  }
2076
- var bytesEq = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)) === 0;
2077
- function assertHttpUrl(url, label) {
2078
- let parsed;
2079
- try {
2080
- parsed = new URL(url);
2081
- } catch {
2082
- throw new ValidationError(`${label} is not a valid URL: ${url}`);
2083
- }
2084
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
2085
- throw new ValidationError(`${label} must use http(s): ${url}`);
2086
- }
811
+ function timingSafeEq(a, b) {
812
+ if (a.length !== b.length) return false;
813
+ return timingSafeEqual(a, b);
2087
814
  }
2088
- async function orchestrateStamp(hash, calendars, networkLayer, logger, signal, minimumSuccessfulSubmissions = 2) {
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) {
2089
817
  if (calendars.length === 0) {
2090
818
  throw new ValidationError("at least one calendar is required to stamp");
2091
819
  }
@@ -2097,7 +825,9 @@ async function orchestrateStamp(hash, calendars, networkLayer, logger, signal, m
2097
825
  `minimumSuccessfulSubmissions (${minimumSuccessfulSubmissions}) cannot exceed the number of calendars (${calendars.length})`
2098
826
  );
2099
827
  }
2100
- for (const url of calendars) assertHttpUrl(url, "calendar");
828
+ await Promise.all(
829
+ calendars.map((url) => assertSafeCalendarUrl(url, { allowPrivate: allowPrivateCalendars }))
830
+ );
2101
831
  const digest = validateHash(hash);
2102
832
  logger?.info(`Starting stamp for ${Buffer.from(digest).toString("hex")}`);
2103
833
  const detached = DetachedTimestampFile.fromHash(new OpSHA256(), digest);
@@ -2169,7 +899,7 @@ async function orchestrateUpgrade(incompleteProof, _calendars, networkLayer, log
2169
899
  }
2170
900
  }
2171
901
  const after = detached.serializeToBytes();
2172
- if (bytesEq(before, after)) {
902
+ if (bytesEqFast(before, after)) {
2173
903
  throw new UpgradeError("No calendar has confirmed the timestamp yet (Bitcoin not yet mined)");
2174
904
  }
2175
905
  return Buffer.from(after);
@@ -2178,42 +908,81 @@ async function orchestrateVerify(proof, networkLayer, originalDataHash, logger,
2178
908
  let detached;
2179
909
  try {
2180
910
  detached = DetachedTimestampFile.deserialize(new Uint8Array(proof));
2181
- } catch {
2182
- return { valid: false, error: "Invalid .ots proof format" };
911
+ } catch (cause) {
912
+ throw new ValidationError("Invalid .ots proof format", {
913
+ cause: cause instanceof Error ? cause : void 0
914
+ });
2183
915
  }
2184
916
  if (originalDataHash !== void 0) {
2185
917
  let expected;
2186
918
  try {
2187
919
  expected = validateHash(originalDataHash);
2188
920
  } catch (err) {
2189
- return { valid: false, error: err instanceof Error ? err.message : "Invalid hash format" };
921
+ throw new ValidationError(
922
+ err instanceof Error ? err.message : "Invalid hash format",
923
+ { cause: err instanceof Error ? err : void 0 }
924
+ );
2190
925
  }
2191
- if (!bytesEq(expected, detached.fileDigest())) {
2192
- return { valid: false, error: "File hash does not match proof" };
926
+ if (!timingSafeEq(expected, detached.fileDigest())) {
927
+ return { status: "invalid", reason: "File hash does not match proof \u2014 file may have been modified" };
2193
928
  }
2194
929
  }
2195
- const bitcoinAtts = detached.timestamp.allAttestations().filter(({ attestation }) => attestation.kind === "bitcoin");
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
+ }
2196
947
  if (bitcoinAtts.length === 0) {
2197
948
  const hasLitecoin = detached.timestamp.allAttestations().some(({ attestation }) => attestation.kind === "litecoin");
2198
- if (hasLitecoin) {
2199
- return { valid: false, error: "Litecoin verification is not supported by this client" };
2200
- }
2201
- return { valid: false, error: "No Bitcoin attestation found (timestamp not yet confirmed)" };
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
+ };
2202
953
  }
2203
954
  const explorer = new EsploraClient(networkLayer);
2204
- let lastError = "";
955
+ let lastNetworkError;
956
+ let lastCryptoError;
2205
957
  for (const { msg, attestation } of bitcoinAtts) {
2206
958
  if (attestation.kind !== "bitcoin") continue;
2207
959
  try {
2208
- const time = await verifyTimestampAttestation(Uint8Array.from(msg).reverse(), attestation, explorer, signal);
960
+ const blockTime = await verifyTimestampAttestation(
961
+ Uint8Array.from(msg).reverse(),
962
+ attestation,
963
+ explorer,
964
+ signal
965
+ );
2209
966
  logger?.info(`Verified against Bitcoin block ${attestation.height}`);
2210
- return { valid: true, blockHeight: attestation.height, timestamp: time };
967
+ return { status: "verified", blockHeight: attestation.height, blockTime };
2211
968
  } catch (err) {
2212
- lastError = err instanceof Error ? err.message : String(err);
2213
- logger?.warn(`Bitcoin attestation at height ${attestation.height} failed: ${lastError}`);
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
+ }
2214
977
  }
2215
978
  }
2216
- return { valid: false, error: `Could not verify against the Bitcoin blockchain: ${lastError}` };
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
+ };
2217
986
  }
2218
987
 
2219
988
  // src/client.ts
@@ -2223,6 +992,7 @@ var OpenTimestampsClient = class {
2223
992
  logger;
2224
993
  globalSignal;
2225
994
  minimumSuccessfulSubmissions;
995
+ allowPrivateCalendars;
2226
996
  /**
2227
997
  * Create a new OpenTimestamps client
2228
998
  *
@@ -2236,6 +1006,7 @@ var OpenTimestampsClient = class {
2236
1006
  this.calendars = options.calendars;
2237
1007
  }
2238
1008
  this.minimumSuccessfulSubmissions = options.minimumSuccessfulSubmissions ?? 2;
1009
+ this.allowPrivateCalendars = options.allowPrivateCalendars ?? false;
2239
1010
  const resilienceConfig = {
2240
1011
  ...DEFAULT_RESILIENCE,
2241
1012
  ...options.resilience,
@@ -2254,7 +1025,8 @@ var OpenTimestampsClient = class {
2254
1025
  };
2255
1026
  this.logger = options.logger;
2256
1027
  this.globalSignal = options.signal;
2257
- this.networkLayer = options.networkLayer ?? new ResilientNetworkLayer(resilienceConfig, this.logger);
1028
+ const internalOptions = options;
1029
+ this.networkLayer = internalOptions._networkLayer ?? new ResilientNetworkLayer(resilienceConfig, this.logger);
2258
1030
  this.logger?.info(`OpenTimestamps client initialized with ${this.calendars.length} calendars`);
2259
1031
  }
2260
1032
  /**
@@ -2283,7 +1055,8 @@ var OpenTimestampsClient = class {
2283
1055
  this.networkLayer,
2284
1056
  this.logger,
2285
1057
  signal,
2286
- this.minimumSuccessfulSubmissions
1058
+ this.minimumSuccessfulSubmissions,
1059
+ this.allowPrivateCalendars
2287
1060
  );
2288
1061
  }
2289
1062
  /**
@@ -2368,6 +1141,10 @@ var OpenTimestampsClient = class {
2368
1141
  }
2369
1142
  };
2370
1143
 
1144
+ // src/index.ts
1145
+ import { DetachedTimestampFile as DetachedTimestampFile2, Timestamp as Timestamp2 } from "@otskit/core";
1146
+ import { verifyAgainstBlockheader as verifyAgainstBlockheader2 } from "@otskit/core";
1147
+
2371
1148
  // src/utils/hash.ts
2372
1149
  import { createHash } from "crypto";
2373
1150
  import { createReadStream } from "fs";
@@ -2390,7 +1167,7 @@ export {
2390
1167
  DEFAULT_CALENDARS,
2391
1168
  DEFAULT_CALENDAR_WHITELIST,
2392
1169
  DEFAULT_RESILIENCE,
2393
- DetachedTimestampFile,
1170
+ DetachedTimestampFile2 as DetachedTimestampFile,
2394
1171
  EsploraClient,
2395
1172
  EsploraResponseError,
2396
1173
  MAX_CALENDAR_RESPONSE_SIZE,
@@ -2400,13 +1177,16 @@ export {
2400
1177
  OpenTimestampsClientError,
2401
1178
  PUBLIC_ESPLORA_URL,
2402
1179
  ResilientNetworkLayer,
1180
+ SizeLimitExceededError,
2403
1181
  StampError,
2404
- Timestamp,
1182
+ Timestamp2 as Timestamp,
2405
1183
  UpgradeError,
2406
1184
  UrlWhitelist,
2407
1185
  ValidationError,
1186
+ assertSafeCalendarUrl,
2408
1187
  hashBuffer,
2409
1188
  hashFile,
2410
- verifyAgainstBlockheader,
1189
+ isVerified,
1190
+ verifyAgainstBlockheader2 as verifyAgainstBlockheader,
2411
1191
  verifyTimestampAttestation
2412
1192
  };