@otskit/client 1.0.0 → 1.0.2
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/LICENSE +21 -21
- package/package.json +3 -4
- package/dist/index.cjs +0 -1012
- package/dist/index.d.cts +0 -361
- package/dist/index.d.ts +0 -361
- package/dist/index.js +0 -965
package/dist/index.cjs
DELETED
|
@@ -1,1012 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/index.ts
|
|
21
|
-
var index_exports = {};
|
|
22
|
-
__export(index_exports, {
|
|
23
|
-
CalendarClient: () => CalendarClient,
|
|
24
|
-
CalendarResponseTooLargeError: () => CalendarResponseTooLargeError,
|
|
25
|
-
CircuitBreakerError: () => CircuitBreakerError,
|
|
26
|
-
CircuitState: () => CircuitState,
|
|
27
|
-
CommitmentNotFoundError: () => CommitmentNotFoundError,
|
|
28
|
-
DEFAULT_AGGREGATORS: () => DEFAULT_AGGREGATORS,
|
|
29
|
-
DEFAULT_CALENDARS: () => DEFAULT_CALENDARS,
|
|
30
|
-
DEFAULT_CALENDAR_WHITELIST: () => DEFAULT_CALENDAR_WHITELIST,
|
|
31
|
-
DEFAULT_RESILIENCE: () => DEFAULT_RESILIENCE,
|
|
32
|
-
DetachedTimestampFile: () => import_core4.DetachedTimestampFile,
|
|
33
|
-
EsploraClient: () => EsploraClient,
|
|
34
|
-
EsploraResponseError: () => EsploraResponseError,
|
|
35
|
-
MAX_CALENDAR_RESPONSE_SIZE: () => MAX_CALENDAR_RESPONSE_SIZE,
|
|
36
|
-
MAX_ESPLORA_RESPONSE_SIZE: () => MAX_ESPLORA_RESPONSE_SIZE,
|
|
37
|
-
NetworkError: () => NetworkError,
|
|
38
|
-
OpenTimestampsClient: () => OpenTimestampsClient,
|
|
39
|
-
OpenTimestampsClientError: () => OpenTimestampsClientError,
|
|
40
|
-
PUBLIC_ESPLORA_URL: () => PUBLIC_ESPLORA_URL,
|
|
41
|
-
ResilientNetworkLayer: () => ResilientNetworkLayer,
|
|
42
|
-
StampError: () => StampError,
|
|
43
|
-
Timestamp: () => import_core4.Timestamp,
|
|
44
|
-
UpgradeError: () => UpgradeError,
|
|
45
|
-
UrlWhitelist: () => UrlWhitelist,
|
|
46
|
-
ValidationError: () => ValidationError,
|
|
47
|
-
verifyAgainstBlockheader: () => import_core5.verifyAgainstBlockheader,
|
|
48
|
-
verifyTimestampAttestation: () => verifyTimestampAttestation
|
|
49
|
-
});
|
|
50
|
-
module.exports = __toCommonJS(index_exports);
|
|
51
|
-
|
|
52
|
-
// src/types.ts
|
|
53
|
-
var DEFAULT_CALENDARS = [
|
|
54
|
-
"https://alice.btc.calendar.opentimestamps.org",
|
|
55
|
-
"https://bob.btc.calendar.opentimestamps.org",
|
|
56
|
-
"https://finney.calendar.eternitywall.com",
|
|
57
|
-
"https://btc.calendar.catallaxy.com"
|
|
58
|
-
];
|
|
59
|
-
var DEFAULT_RESILIENCE = {
|
|
60
|
-
totalTimeoutMs: 3e4,
|
|
61
|
-
connectTimeoutMs: 5e3,
|
|
62
|
-
retries: {
|
|
63
|
-
enabled: true,
|
|
64
|
-
maxAttempts: 3,
|
|
65
|
-
backoff: {
|
|
66
|
-
strategy: "exponential",
|
|
67
|
-
initialDelayMs: 200,
|
|
68
|
-
maxDelayMs: 5e3,
|
|
69
|
-
jitter: "full"
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
circuitBreaker: {
|
|
73
|
-
enabled: true,
|
|
74
|
-
failureThreshold: 5,
|
|
75
|
-
recoveryTimeoutMs: 15e3,
|
|
76
|
-
halfOpenMaxAttempts: 1
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// src/errors.ts
|
|
81
|
-
var OpenTimestampsClientError = class extends Error {
|
|
82
|
-
cause;
|
|
83
|
-
constructor(message, options) {
|
|
84
|
-
super(message);
|
|
85
|
-
this.name = this.constructor.name;
|
|
86
|
-
this.cause = options?.cause;
|
|
87
|
-
Error.captureStackTrace?.(this, this.constructor);
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
var ValidationError = class extends OpenTimestampsClientError {
|
|
91
|
-
};
|
|
92
|
-
var StampError = class extends OpenTimestampsClientError {
|
|
93
|
-
successfulSubmissions;
|
|
94
|
-
failedSubmissions;
|
|
95
|
-
constructor(message, successful, failed, options) {
|
|
96
|
-
super(message, options);
|
|
97
|
-
this.successfulSubmissions = successful;
|
|
98
|
-
this.failedSubmissions = failed;
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
var UpgradeError = class extends OpenTimestampsClientError {
|
|
102
|
-
};
|
|
103
|
-
var NetworkError = class extends OpenTimestampsClientError {
|
|
104
|
-
/** HTTP status code, cuando el fallo viene de una respuesta HTTP. */
|
|
105
|
-
status;
|
|
106
|
-
constructor(message, options) {
|
|
107
|
-
super(message, options);
|
|
108
|
-
this.status = options?.status;
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
var CircuitBreakerError = class extends NetworkError {
|
|
112
|
-
constructor(calendar) {
|
|
113
|
-
super(`Circuit breaker open for calendar: ${calendar}`);
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
var CommitmentNotFoundError = class extends NetworkError {
|
|
117
|
-
};
|
|
118
|
-
var CalendarResponseTooLargeError = class extends NetworkError {
|
|
119
|
-
};
|
|
120
|
-
var EsploraResponseError = class extends NetworkError {
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
// src/network/circuit-breaker.ts
|
|
124
|
-
var CircuitState = /* @__PURE__ */ ((CircuitState2) => {
|
|
125
|
-
CircuitState2["CLOSED"] = "CLOSED";
|
|
126
|
-
CircuitState2["OPEN"] = "OPEN";
|
|
127
|
-
CircuitState2["HALF_OPEN"] = "HALF_OPEN";
|
|
128
|
-
return CircuitState2;
|
|
129
|
-
})(CircuitState || {});
|
|
130
|
-
var CircuitBreaker = class {
|
|
131
|
-
constructor(options, logger) {
|
|
132
|
-
this.options = options;
|
|
133
|
-
this.logger = logger;
|
|
134
|
-
}
|
|
135
|
-
options;
|
|
136
|
-
logger;
|
|
137
|
-
circuits = /* @__PURE__ */ new Map();
|
|
138
|
-
/**
|
|
139
|
-
* Execute a request through the circuit breaker
|
|
140
|
-
*/
|
|
141
|
-
async execute(key, fn) {
|
|
142
|
-
if (!this.options.enabled) {
|
|
143
|
-
return fn();
|
|
144
|
-
}
|
|
145
|
-
const circuit = this.getOrCreateCircuit(key);
|
|
146
|
-
if (circuit.state === "OPEN" /* OPEN */) {
|
|
147
|
-
const shouldAttemptRecovery = this.shouldAttemptRecovery(circuit);
|
|
148
|
-
if (shouldAttemptRecovery) {
|
|
149
|
-
this.logger?.info(`Circuit breaker for ${key} entering HALF_OPEN state`);
|
|
150
|
-
circuit.state = "HALF_OPEN" /* HALF_OPEN */;
|
|
151
|
-
circuit.stats.halfOpenAttempts = 0;
|
|
152
|
-
} else {
|
|
153
|
-
throw new CircuitBreakerError(key);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
if (circuit.state === "HALF_OPEN" /* HALF_OPEN */) {
|
|
157
|
-
const maxAttempts = this.options.halfOpenMaxAttempts || 1;
|
|
158
|
-
if (circuit.stats.halfOpenAttempts >= maxAttempts) {
|
|
159
|
-
this.logger?.warn(`Circuit breaker for ${key} reopening after failed HALF_OPEN attempts`);
|
|
160
|
-
circuit.state = "OPEN" /* OPEN */;
|
|
161
|
-
circuit.stats.lastFailureTime = Date.now();
|
|
162
|
-
throw new CircuitBreakerError(key);
|
|
163
|
-
}
|
|
164
|
-
circuit.stats.halfOpenAttempts++;
|
|
165
|
-
}
|
|
166
|
-
try {
|
|
167
|
-
const result = await fn();
|
|
168
|
-
this.onSuccess(key, circuit);
|
|
169
|
-
return result;
|
|
170
|
-
} catch (error) {
|
|
171
|
-
this.onFailure(key, circuit);
|
|
172
|
-
throw error;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
getOrCreateCircuit(key) {
|
|
176
|
-
let circuit = this.circuits.get(key);
|
|
177
|
-
if (!circuit) {
|
|
178
|
-
circuit = {
|
|
179
|
-
state: "CLOSED" /* CLOSED */,
|
|
180
|
-
stats: {
|
|
181
|
-
consecutiveFailures: 0,
|
|
182
|
-
halfOpenAttempts: 0
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
this.circuits.set(key, circuit);
|
|
186
|
-
}
|
|
187
|
-
return circuit;
|
|
188
|
-
}
|
|
189
|
-
shouldAttemptRecovery(circuit) {
|
|
190
|
-
if (!circuit.stats.lastFailureTime) return false;
|
|
191
|
-
const elapsed = Date.now() - circuit.stats.lastFailureTime;
|
|
192
|
-
return elapsed >= this.options.recoveryTimeoutMs;
|
|
193
|
-
}
|
|
194
|
-
onSuccess(key, circuit) {
|
|
195
|
-
if (circuit.state === "HALF_OPEN" /* HALF_OPEN */) {
|
|
196
|
-
this.logger?.info(`Circuit breaker for ${key} closing after successful HALF_OPEN attempt`);
|
|
197
|
-
circuit.state = "CLOSED" /* CLOSED */;
|
|
198
|
-
}
|
|
199
|
-
circuit.stats.consecutiveFailures = 0;
|
|
200
|
-
circuit.stats.halfOpenAttempts = 0;
|
|
201
|
-
}
|
|
202
|
-
onFailure(key, circuit) {
|
|
203
|
-
circuit.stats.consecutiveFailures++;
|
|
204
|
-
circuit.stats.lastFailureTime = Date.now();
|
|
205
|
-
if (circuit.state === "HALF_OPEN" /* HALF_OPEN */) {
|
|
206
|
-
this.logger?.warn(`Circuit breaker for ${key} reopening after failed HALF_OPEN attempt`);
|
|
207
|
-
circuit.state = "OPEN" /* OPEN */;
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
if (circuit.stats.consecutiveFailures >= this.options.failureThreshold) {
|
|
211
|
-
this.logger?.warn(
|
|
212
|
-
`Circuit breaker for ${key} opening after ${circuit.stats.consecutiveFailures} consecutive failures`
|
|
213
|
-
);
|
|
214
|
-
circuit.state = "OPEN" /* OPEN */;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
/** Get current state for debugging/monitoring */
|
|
218
|
-
getState(key) {
|
|
219
|
-
return this.circuits.get(key)?.state;
|
|
220
|
-
}
|
|
221
|
-
/** Reset a specific circuit */
|
|
222
|
-
reset(key) {
|
|
223
|
-
this.circuits.delete(key);
|
|
224
|
-
}
|
|
225
|
-
/** Reset all circuits */
|
|
226
|
-
resetAll() {
|
|
227
|
-
this.circuits.clear();
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
// src/network/retry.ts
|
|
232
|
-
function calculateDelay(attempt, options) {
|
|
233
|
-
const { strategy, initialDelayMs, maxDelayMs, jitter } = options.backoff;
|
|
234
|
-
let delay;
|
|
235
|
-
switch (strategy) {
|
|
236
|
-
case "exponential":
|
|
237
|
-
delay = initialDelayMs * Math.pow(2, attempt - 1);
|
|
238
|
-
break;
|
|
239
|
-
case "linear":
|
|
240
|
-
delay = initialDelayMs * attempt;
|
|
241
|
-
break;
|
|
242
|
-
case "constant":
|
|
243
|
-
delay = initialDelayMs;
|
|
244
|
-
break;
|
|
245
|
-
}
|
|
246
|
-
if (maxDelayMs && delay > maxDelayMs) {
|
|
247
|
-
delay = maxDelayMs;
|
|
248
|
-
}
|
|
249
|
-
switch (jitter) {
|
|
250
|
-
case "full":
|
|
251
|
-
delay = Math.random() * delay;
|
|
252
|
-
break;
|
|
253
|
-
case "equal":
|
|
254
|
-
delay = delay / 2 + Math.random() * (delay / 2);
|
|
255
|
-
break;
|
|
256
|
-
case "none":
|
|
257
|
-
default:
|
|
258
|
-
break;
|
|
259
|
-
}
|
|
260
|
-
return Math.floor(delay);
|
|
261
|
-
}
|
|
262
|
-
function sleep(ms, signal) {
|
|
263
|
-
return new Promise((resolve, reject) => {
|
|
264
|
-
if (signal?.aborted) {
|
|
265
|
-
reject(new Error("Aborted"));
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
const timeout = setTimeout(resolve, ms);
|
|
269
|
-
signal?.addEventListener("abort", () => {
|
|
270
|
-
clearTimeout(timeout);
|
|
271
|
-
reject(new Error("Aborted"));
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
async function withRetry(fn, options, logger, signal) {
|
|
276
|
-
if (!options.enabled) {
|
|
277
|
-
return fn();
|
|
278
|
-
}
|
|
279
|
-
let lastError;
|
|
280
|
-
for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
|
|
281
|
-
try {
|
|
282
|
-
logger?.debug(`Attempt ${attempt}/${options.maxAttempts}`);
|
|
283
|
-
return await fn();
|
|
284
|
-
} catch (error) {
|
|
285
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
286
|
-
if (signal?.aborted) {
|
|
287
|
-
throw lastError;
|
|
288
|
-
}
|
|
289
|
-
if (error.retryable === false) {
|
|
290
|
-
logger?.debug("Error is not retryable (4xx client error), failing immediately");
|
|
291
|
-
throw lastError;
|
|
292
|
-
}
|
|
293
|
-
if (attempt === options.maxAttempts) {
|
|
294
|
-
logger?.warn(`All ${options.maxAttempts} attempts failed`);
|
|
295
|
-
throw lastError;
|
|
296
|
-
}
|
|
297
|
-
const delay = calculateDelay(attempt, options);
|
|
298
|
-
logger?.debug(`Retry attempt ${attempt} failed, waiting ${delay}ms before next attempt`);
|
|
299
|
-
try {
|
|
300
|
-
await sleep(delay, signal);
|
|
301
|
-
} catch {
|
|
302
|
-
throw lastError;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
throw lastError || new Error("Retry failed");
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// src/adapters/fetch-adapter.ts
|
|
310
|
-
async function executeRequest(request) {
|
|
311
|
-
try {
|
|
312
|
-
const response = await globalThis.fetch(request.url, {
|
|
313
|
-
method: request.method,
|
|
314
|
-
headers: {
|
|
315
|
-
"Content-Type": "application/octet-stream",
|
|
316
|
-
...request.headers
|
|
317
|
-
},
|
|
318
|
-
body: request.body,
|
|
319
|
-
signal: request.signal
|
|
320
|
-
});
|
|
321
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
322
|
-
const data = new Uint8Array(arrayBuffer);
|
|
323
|
-
return {
|
|
324
|
-
ok: response.ok,
|
|
325
|
-
status: response.status,
|
|
326
|
-
statusText: response.statusText,
|
|
327
|
-
data
|
|
328
|
-
};
|
|
329
|
-
} catch (error) {
|
|
330
|
-
if (error instanceof Error) {
|
|
331
|
-
if (error.name === "AbortError") {
|
|
332
|
-
throw new NetworkError("Request aborted", { cause: error });
|
|
333
|
-
}
|
|
334
|
-
if (error.message.includes("timeout")) {
|
|
335
|
-
throw new NetworkError("Request timeout", { cause: error });
|
|
336
|
-
}
|
|
337
|
-
throw new NetworkError(`Network request failed: ${error.message}`, { cause: error });
|
|
338
|
-
}
|
|
339
|
-
throw new NetworkError("Unknown network error");
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
function createTimeoutController(timeoutMs, parentSignal) {
|
|
343
|
-
const controller = new AbortController();
|
|
344
|
-
const timeout = setTimeout(() => {
|
|
345
|
-
controller.abort(new Error("Timeout"));
|
|
346
|
-
}, timeoutMs);
|
|
347
|
-
if (parentSignal) {
|
|
348
|
-
if (parentSignal.aborted) {
|
|
349
|
-
clearTimeout(timeout);
|
|
350
|
-
controller.abort(parentSignal.reason);
|
|
351
|
-
} else {
|
|
352
|
-
parentSignal.addEventListener("abort", () => {
|
|
353
|
-
clearTimeout(timeout);
|
|
354
|
-
controller.abort(parentSignal.reason);
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
controller.signal.addEventListener("abort", () => {
|
|
359
|
-
clearTimeout(timeout);
|
|
360
|
-
});
|
|
361
|
-
return controller;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// src/network/resilience.ts
|
|
365
|
-
var ResilientNetworkLayer = class {
|
|
366
|
-
constructor(options, logger) {
|
|
367
|
-
this.options = options;
|
|
368
|
-
this.logger = logger;
|
|
369
|
-
this.circuitBreaker = new CircuitBreaker(options.circuitBreaker, logger);
|
|
370
|
-
}
|
|
371
|
-
options;
|
|
372
|
-
logger;
|
|
373
|
-
circuitBreaker;
|
|
374
|
-
/**
|
|
375
|
-
* Execute a request with full resilience pipeline
|
|
376
|
-
*/
|
|
377
|
-
async request(calendarUrl, request, parentSignal) {
|
|
378
|
-
const startTime = Date.now();
|
|
379
|
-
const totalController = createTimeoutController(
|
|
380
|
-
this.options.totalTimeoutMs,
|
|
381
|
-
parentSignal
|
|
382
|
-
);
|
|
383
|
-
try {
|
|
384
|
-
return await this.circuitBreaker.execute(calendarUrl, async () => {
|
|
385
|
-
return await withRetry(
|
|
386
|
-
async () => {
|
|
387
|
-
const attemptController = createTimeoutController(
|
|
388
|
-
this.options.connectTimeoutMs,
|
|
389
|
-
totalController.signal
|
|
390
|
-
);
|
|
391
|
-
try {
|
|
392
|
-
const response = await executeRequest({
|
|
393
|
-
...request,
|
|
394
|
-
signal: attemptController.signal
|
|
395
|
-
});
|
|
396
|
-
const elapsed = Date.now() - startTime;
|
|
397
|
-
this.logger?.debug(`Request to ${calendarUrl} succeeded in ${elapsed}ms`);
|
|
398
|
-
if (!response.ok) {
|
|
399
|
-
if (response.status >= 400 && response.status < 500) {
|
|
400
|
-
const error = new NetworkError(
|
|
401
|
-
`HTTP ${response.status}: ${response.statusText}`,
|
|
402
|
-
{ status: response.status }
|
|
403
|
-
);
|
|
404
|
-
error.retryable = false;
|
|
405
|
-
throw error;
|
|
406
|
-
}
|
|
407
|
-
throw new NetworkError(
|
|
408
|
-
`HTTP ${response.status}: ${response.statusText}`,
|
|
409
|
-
{ status: response.status }
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
return response;
|
|
413
|
-
} finally {
|
|
414
|
-
attemptController.signal.removeEventListener("abort", () => {
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
},
|
|
418
|
-
this.options.retries,
|
|
419
|
-
this.logger,
|
|
420
|
-
totalController.signal
|
|
421
|
-
);
|
|
422
|
-
});
|
|
423
|
-
} catch (error) {
|
|
424
|
-
const elapsed = Date.now() - startTime;
|
|
425
|
-
this.logger?.error(`Request to ${calendarUrl} failed after ${elapsed}ms`, error);
|
|
426
|
-
throw error;
|
|
427
|
-
} finally {
|
|
428
|
-
totalController.signal.removeEventListener("abort", () => {
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
/** Get circuit breaker state for a calendar */
|
|
433
|
-
getCircuitState(calendarUrl) {
|
|
434
|
-
return this.circuitBreaker.getState(calendarUrl);
|
|
435
|
-
}
|
|
436
|
-
/** Reset circuit breaker for a calendar */
|
|
437
|
-
resetCircuit(calendarUrl) {
|
|
438
|
-
this.circuitBreaker.reset(calendarUrl);
|
|
439
|
-
}
|
|
440
|
-
/** Reset all circuit breakers */
|
|
441
|
-
resetAllCircuits() {
|
|
442
|
-
this.circuitBreaker.resetAll();
|
|
443
|
-
}
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
// src/core/orchestration.ts
|
|
447
|
-
var import_core3 = require("@otskit/core");
|
|
448
|
-
|
|
449
|
-
// src/network/calendar.ts
|
|
450
|
-
var import_core = require("@otskit/core");
|
|
451
|
-
var MAX_CALENDAR_RESPONSE_SIZE = 1e4;
|
|
452
|
-
function assertCommitment(bytes) {
|
|
453
|
-
if (!(bytes instanceof Uint8Array)) {
|
|
454
|
-
throw new TypeError("commitment must be a Uint8Array");
|
|
455
|
-
}
|
|
456
|
-
if (bytes.length === 0 || bytes.length > 64) {
|
|
457
|
-
throw new RangeError(`commitment length ${bytes.length} is out of range (1..64)`);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
var OTS_HEADERS = {
|
|
461
|
-
Accept: "application/vnd.opentimestamps.v1",
|
|
462
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
463
|
-
};
|
|
464
|
-
function joinUrl(base, path) {
|
|
465
|
-
return base.replace(/\/+$/, "") + path;
|
|
466
|
-
}
|
|
467
|
-
var CalendarClient = class {
|
|
468
|
-
constructor(url, networkLayer, logger) {
|
|
469
|
-
this.url = url;
|
|
470
|
-
this.networkLayer = networkLayer;
|
|
471
|
-
this.logger = logger;
|
|
472
|
-
}
|
|
473
|
-
url;
|
|
474
|
-
networkLayer;
|
|
475
|
-
logger;
|
|
476
|
-
/** Envía un digest al calendario y devuelve el Timestamp que lo commit-ea. */
|
|
477
|
-
async submit(digest, signal) {
|
|
478
|
-
assertCommitment(digest);
|
|
479
|
-
this.logger?.debug(`Submitting digest to ${this.url}/digest`);
|
|
480
|
-
const response = await this.networkLayer.request(
|
|
481
|
-
this.url,
|
|
482
|
-
{ url: joinUrl(this.url, "/digest"), method: "POST", headers: OTS_HEADERS, body: digest },
|
|
483
|
-
signal
|
|
484
|
-
);
|
|
485
|
-
return this.#parseTimestamp(response.data, digest);
|
|
486
|
-
}
|
|
487
|
-
/** Pregunta al calendario si tiene un Timestamp más completo para `commitment` (upgrade). */
|
|
488
|
-
async getTimestamp(commitment, signal) {
|
|
489
|
-
assertCommitment(commitment);
|
|
490
|
-
const path = `/timestamp/${(0, import_core.bytesToHex)(commitment)}`;
|
|
491
|
-
this.logger?.debug(`Querying ${this.url}${path}`);
|
|
492
|
-
let response;
|
|
493
|
-
try {
|
|
494
|
-
response = await this.networkLayer.request(
|
|
495
|
-
this.url,
|
|
496
|
-
{ url: joinUrl(this.url, path), method: "GET", headers: OTS_HEADERS },
|
|
497
|
-
signal
|
|
498
|
-
);
|
|
499
|
-
} catch (err) {
|
|
500
|
-
if (err instanceof NetworkError && err.status === 404) {
|
|
501
|
-
throw new CommitmentNotFoundError(`calendar ${this.url} has no timestamp for the commitment yet`, {
|
|
502
|
-
cause: err
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
throw err;
|
|
506
|
-
}
|
|
507
|
-
return this.#parseTimestamp(response.data, commitment);
|
|
508
|
-
}
|
|
509
|
-
/** Deserializa la respuesta del calendario como un Timestamp commit-eado a `commitment`. */
|
|
510
|
-
#parseTimestamp(data, commitment) {
|
|
511
|
-
if (data.length > MAX_CALENDAR_RESPONSE_SIZE) {
|
|
512
|
-
throw new CalendarResponseTooLargeError(
|
|
513
|
-
`calendar response of ${data.length} bytes exceeds limit ${MAX_CALENDAR_RESPONSE_SIZE}`
|
|
514
|
-
);
|
|
515
|
-
}
|
|
516
|
-
const ctx = new import_core.StreamDeserializationContext(data);
|
|
517
|
-
const timestamp = import_core.Timestamp.deserialize(ctx, commitment);
|
|
518
|
-
ctx.assertEof();
|
|
519
|
-
return timestamp;
|
|
520
|
-
}
|
|
521
|
-
};
|
|
522
|
-
function wildcardToRegExp(pattern) {
|
|
523
|
-
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, "[^/]*");
|
|
524
|
-
return new RegExp(`^${escaped}$`, "i");
|
|
525
|
-
}
|
|
526
|
-
var UrlWhitelist = class {
|
|
527
|
-
#patterns = /* @__PURE__ */ new Set();
|
|
528
|
-
constructor(urls) {
|
|
529
|
-
if (urls) {
|
|
530
|
-
for (const u of urls) this.add(u);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
/** Añade un patrón; si no trae esquema, se añaden las variantes http y https. */
|
|
534
|
-
add(url) {
|
|
535
|
-
if (typeof url !== "string") {
|
|
536
|
-
throw new TypeError("UrlWhitelist: URL must be a string");
|
|
537
|
-
}
|
|
538
|
-
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
539
|
-
this.#patterns.add(url);
|
|
540
|
-
} else {
|
|
541
|
-
this.#patterns.add("http://" + url);
|
|
542
|
-
this.#patterns.add("https://" + url);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
/** Verdadero si `url` casa con algún patrón de la whitelist. */
|
|
546
|
-
contains(url) {
|
|
547
|
-
for (const pattern of this.#patterns) {
|
|
548
|
-
if (wildcardToRegExp(pattern).test(url)) return true;
|
|
549
|
-
}
|
|
550
|
-
return false;
|
|
551
|
-
}
|
|
552
|
-
toString() {
|
|
553
|
-
return `UrlWhitelist([${[...this.#patterns].join(", ")}])`;
|
|
554
|
-
}
|
|
555
|
-
};
|
|
556
|
-
var DEFAULT_CALENDAR_WHITELIST = new UrlWhitelist([
|
|
557
|
-
"https://*.calendar.opentimestamps.org",
|
|
558
|
-
// Peter Todd
|
|
559
|
-
"https://*.calendar.eternitywall.com",
|
|
560
|
-
// Eternity Wall
|
|
561
|
-
"https://*.calendar.catallaxy.com"
|
|
562
|
-
// Catallaxy
|
|
563
|
-
]);
|
|
564
|
-
var DEFAULT_AGGREGATORS = [
|
|
565
|
-
"https://a.pool.opentimestamps.org",
|
|
566
|
-
"https://b.pool.opentimestamps.org",
|
|
567
|
-
"https://a.pool.eternitywall.com",
|
|
568
|
-
"https://ots.btc.catallaxy.com"
|
|
569
|
-
];
|
|
570
|
-
|
|
571
|
-
// src/network/esplora.ts
|
|
572
|
-
var import_core2 = require("@otskit/core");
|
|
573
|
-
var PUBLIC_ESPLORA_URL = "https://blockstream.info/api";
|
|
574
|
-
var MAX_ESPLORA_RESPONSE_SIZE = 1e5;
|
|
575
|
-
var HEX64_RE = /^[0-9a-f]{64}$/i;
|
|
576
|
-
var EsploraClient = class {
|
|
577
|
-
#url;
|
|
578
|
-
#networkLayer;
|
|
579
|
-
#logger;
|
|
580
|
-
constructor(networkLayer, options = {}) {
|
|
581
|
-
const raw = options.url ?? PUBLIC_ESPLORA_URL;
|
|
582
|
-
let parsed;
|
|
583
|
-
try {
|
|
584
|
-
parsed = new URL(raw);
|
|
585
|
-
} catch {
|
|
586
|
-
throw new ValidationError(`invalid Esplora URL: ${raw}`);
|
|
587
|
-
}
|
|
588
|
-
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
589
|
-
throw new ValidationError(`Esplora URL must use http(s): ${raw}`);
|
|
590
|
-
}
|
|
591
|
-
this.#networkLayer = networkLayer;
|
|
592
|
-
this.#url = raw.replace(/\/+$/, "");
|
|
593
|
-
this.#logger = options.logger;
|
|
594
|
-
}
|
|
595
|
-
/** Devuelve el hash (hex 64, minúsculas) del bloque a la altura dada. */
|
|
596
|
-
async blockHash(height, signal) {
|
|
597
|
-
if (!Number.isSafeInteger(height) || height < 0) {
|
|
598
|
-
throw new ValidationError(`block height must be a non-negative safe integer; got ${height}`);
|
|
599
|
-
}
|
|
600
|
-
this.#logger?.debug(`Esplora block-height ${height}`);
|
|
601
|
-
const response = await this.#networkLayer.request(
|
|
602
|
-
this.#url,
|
|
603
|
-
{ url: `${this.#url}/block-height/${height}`, method: "GET", headers: { Accept: "text/plain" } },
|
|
604
|
-
signal
|
|
605
|
-
);
|
|
606
|
-
const text = this.#decode(response.data).trim();
|
|
607
|
-
if (!HEX64_RE.test(text)) {
|
|
608
|
-
throw new EsploraResponseError(`esplora returned an invalid block hash for height ${height}`);
|
|
609
|
-
}
|
|
610
|
-
return text.toLowerCase();
|
|
611
|
-
}
|
|
612
|
-
/** Devuelve la cabecera del bloque (merkleroot + time) dado su hash. */
|
|
613
|
-
async block(hash, signal) {
|
|
614
|
-
if (typeof hash !== "string" || !HEX64_RE.test(hash)) {
|
|
615
|
-
throw new ValidationError("block hash must be a 64-char hex string");
|
|
616
|
-
}
|
|
617
|
-
this.#logger?.debug(`Esplora block ${hash}`);
|
|
618
|
-
const response = await this.#networkLayer.request(
|
|
619
|
-
this.#url,
|
|
620
|
-
{ url: `${this.#url}/block/${hash}`, method: "GET", headers: { Accept: "application/json" } },
|
|
621
|
-
signal
|
|
622
|
-
);
|
|
623
|
-
const text = this.#decode(response.data);
|
|
624
|
-
let body;
|
|
625
|
-
try {
|
|
626
|
-
body = JSON.parse(text);
|
|
627
|
-
} catch (err) {
|
|
628
|
-
throw new EsploraResponseError("esplora returned a non-JSON block response", {
|
|
629
|
-
/* v8 ignore next */
|
|
630
|
-
cause: err instanceof Error ? err : void 0
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
if (typeof body !== "object" || body === null) {
|
|
634
|
-
throw new EsploraResponseError("esplora block response is not an object");
|
|
635
|
-
}
|
|
636
|
-
const { merkle_root: merkleroot, timestamp: time } = body;
|
|
637
|
-
if (typeof merkleroot !== "string" || !HEX64_RE.test(merkleroot)) {
|
|
638
|
-
throw new EsploraResponseError("esplora block merkle_root is not a 64-char hex string");
|
|
639
|
-
}
|
|
640
|
-
if (typeof time !== "number" || !Number.isInteger(time) || time <= 0) {
|
|
641
|
-
throw new EsploraResponseError("esplora block timestamp is not a positive integer");
|
|
642
|
-
}
|
|
643
|
-
return { merkleroot, time };
|
|
644
|
-
}
|
|
645
|
-
/** Decodifica el cuerpo a texto aplicando el límite de tamaño (fail-closed). */
|
|
646
|
-
#decode(data) {
|
|
647
|
-
if (data.length > MAX_ESPLORA_RESPONSE_SIZE) {
|
|
648
|
-
throw new EsploraResponseError(
|
|
649
|
-
`esplora response of ${data.length} bytes exceeds limit ${MAX_ESPLORA_RESPONSE_SIZE}`
|
|
650
|
-
);
|
|
651
|
-
}
|
|
652
|
-
return new TextDecoder("utf-8", { fatal: false }).decode(data);
|
|
653
|
-
}
|
|
654
|
-
};
|
|
655
|
-
async function verifyTimestampAttestation(digest, attestation, explorer, signal) {
|
|
656
|
-
if (attestation.kind !== "bitcoin" && attestation.kind !== "litecoin") {
|
|
657
|
-
throw new import_core2.VerificationError(`cannot verify a '${attestation.kind}' attestation against the chain`);
|
|
658
|
-
}
|
|
659
|
-
const hash = await explorer.blockHash(attestation.height, signal);
|
|
660
|
-
const header = await explorer.block(hash, signal);
|
|
661
|
-
return (0, import_core2.verifyAgainstBlockheader)(digest, header);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// src/core/orchestration.ts
|
|
665
|
-
function validateHash(hash) {
|
|
666
|
-
if (typeof hash === "string") {
|
|
667
|
-
const hex = hash.trim().toLowerCase();
|
|
668
|
-
if (!/^[0-9a-f]{64}$/.test(hex)) {
|
|
669
|
-
throw new ValidationError("Hash must be a 64-character hex string (SHA-256)");
|
|
670
|
-
}
|
|
671
|
-
return Uint8Array.from(Buffer.from(hex, "hex"));
|
|
672
|
-
}
|
|
673
|
-
if (hash.length !== 32) {
|
|
674
|
-
throw new ValidationError("Hash must be exactly 32 bytes (SHA-256)");
|
|
675
|
-
}
|
|
676
|
-
return Uint8Array.from(hash);
|
|
677
|
-
}
|
|
678
|
-
function secureNonce(n) {
|
|
679
|
-
const bytes = new Uint8Array(n);
|
|
680
|
-
if (!globalThis.crypto?.getRandomValues) {
|
|
681
|
-
throw new Error("secure RNG unavailable: globalThis.crypto.getRandomValues is required");
|
|
682
|
-
}
|
|
683
|
-
globalThis.crypto.getRandomValues(bytes);
|
|
684
|
-
return bytes;
|
|
685
|
-
}
|
|
686
|
-
var bytesEq = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)) === 0;
|
|
687
|
-
function assertHttpUrl(url, label) {
|
|
688
|
-
let parsed;
|
|
689
|
-
try {
|
|
690
|
-
parsed = new URL(url);
|
|
691
|
-
} catch {
|
|
692
|
-
throw new ValidationError(`${label} is not a valid URL: ${url}`);
|
|
693
|
-
}
|
|
694
|
-
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
695
|
-
throw new ValidationError(`${label} must use http(s): ${url}`);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
async function orchestrateStamp(hash, calendars, networkLayer, logger, signal, minimumSuccessfulSubmissions = 2) {
|
|
699
|
-
if (calendars.length === 0) {
|
|
700
|
-
throw new ValidationError("at least one calendar is required to stamp");
|
|
701
|
-
}
|
|
702
|
-
if (!Number.isInteger(minimumSuccessfulSubmissions) || minimumSuccessfulSubmissions < 1) {
|
|
703
|
-
throw new ValidationError("minimumSuccessfulSubmissions must be an integer >= 1");
|
|
704
|
-
}
|
|
705
|
-
if (minimumSuccessfulSubmissions > calendars.length) {
|
|
706
|
-
throw new ValidationError(
|
|
707
|
-
`minimumSuccessfulSubmissions (${minimumSuccessfulSubmissions}) cannot exceed the number of calendars (${calendars.length})`
|
|
708
|
-
);
|
|
709
|
-
}
|
|
710
|
-
for (const url of calendars) assertHttpUrl(url, "calendar");
|
|
711
|
-
const digest = validateHash(hash);
|
|
712
|
-
logger?.info(`Starting stamp for ${Buffer.from(digest).toString("hex")}`);
|
|
713
|
-
const detached = import_core3.DetachedTimestampFile.fromHash(new import_core3.OpSHA256(), digest);
|
|
714
|
-
const nonceAppended = detached.timestamp.add(new import_core3.OpAppend(secureNonce(16)));
|
|
715
|
-
const merkleRoot = nonceAppended.add(new import_core3.OpSHA256());
|
|
716
|
-
const merkleTip = (0, import_core3.makeMerkleTree)([merkleRoot]);
|
|
717
|
-
const results = await Promise.allSettled(
|
|
718
|
-
calendars.map((url) => new CalendarClient(url, networkLayer, logger).submit(merkleTip.getDigest(), signal))
|
|
719
|
-
);
|
|
720
|
-
const successful = [];
|
|
721
|
-
const failed = [];
|
|
722
|
-
results.forEach((r, i) => {
|
|
723
|
-
const calendar = calendars[i];
|
|
724
|
-
if (r.status === "fulfilled") {
|
|
725
|
-
merkleTip.merge(r.value);
|
|
726
|
-
successful.push({ calendar });
|
|
727
|
-
logger?.info(`Submitted to ${calendar}`);
|
|
728
|
-
} else {
|
|
729
|
-
const error = r.reason instanceof Error ? r.reason : new Error(String(r.reason));
|
|
730
|
-
failed.push({ calendar, error });
|
|
731
|
-
logger?.warn(`Failed to submit to ${calendar}: ${error.message}`);
|
|
732
|
-
}
|
|
733
|
-
});
|
|
734
|
-
if (successful.length < minimumSuccessfulSubmissions) {
|
|
735
|
-
throw new StampError(
|
|
736
|
-
`Insufficient successful submissions (${successful.length}/${minimumSuccessfulSubmissions} required)`,
|
|
737
|
-
successful,
|
|
738
|
-
failed
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
|
-
return Buffer.from(detached.serializeToBytes());
|
|
742
|
-
}
|
|
743
|
-
async function orchestrateUpgrade(incompleteProof, _calendars, networkLayer, logger, signal) {
|
|
744
|
-
let detached;
|
|
745
|
-
try {
|
|
746
|
-
detached = import_core3.DetachedTimestampFile.deserialize(new Uint8Array(incompleteProof));
|
|
747
|
-
} catch (error) {
|
|
748
|
-
throw new ValidationError("Invalid .ots proof format", {
|
|
749
|
-
/* v8 ignore next */
|
|
750
|
-
cause: error instanceof Error ? error : void 0
|
|
751
|
-
});
|
|
752
|
-
}
|
|
753
|
-
if (detached.timestamp.isTimestampComplete()) {
|
|
754
|
-
logger?.info("Proof already complete; nothing to upgrade");
|
|
755
|
-
return Buffer.from(incompleteProof);
|
|
756
|
-
}
|
|
757
|
-
const before = detached.serializeToBytes();
|
|
758
|
-
for (const subStamp of detached.timestamp.directlyVerified()) {
|
|
759
|
-
if (subStamp.isTimestampComplete()) continue;
|
|
760
|
-
for (const att of subStamp.attestations) {
|
|
761
|
-
if (att.kind !== "pending") continue;
|
|
762
|
-
if (!DEFAULT_CALENDAR_WHITELIST.contains(att.uri)) {
|
|
763
|
-
logger?.warn(`Ignoring attestation from non-whitelisted calendar ${att.uri}`);
|
|
764
|
-
continue;
|
|
765
|
-
}
|
|
766
|
-
try {
|
|
767
|
-
const upgraded = await new CalendarClient(att.uri, networkLayer, logger).getTimestamp(
|
|
768
|
-
subStamp.getDigest(),
|
|
769
|
-
signal
|
|
770
|
-
);
|
|
771
|
-
subStamp.merge(upgraded);
|
|
772
|
-
} catch (err) {
|
|
773
|
-
if (err instanceof CommitmentNotFoundError) {
|
|
774
|
-
logger?.debug(`Calendar ${att.uri} has not confirmed yet`);
|
|
775
|
-
continue;
|
|
776
|
-
}
|
|
777
|
-
logger?.warn(`Failed to query ${att.uri}: ${err instanceof Error ? err.message : String(err)}`);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
const after = detached.serializeToBytes();
|
|
782
|
-
if (bytesEq(before, after)) {
|
|
783
|
-
throw new UpgradeError("No calendar has confirmed the timestamp yet (Bitcoin not yet mined)");
|
|
784
|
-
}
|
|
785
|
-
return Buffer.from(after);
|
|
786
|
-
}
|
|
787
|
-
async function orchestrateVerify(proof, networkLayer, originalDataHash, logger, signal) {
|
|
788
|
-
let detached;
|
|
789
|
-
try {
|
|
790
|
-
detached = import_core3.DetachedTimestampFile.deserialize(new Uint8Array(proof));
|
|
791
|
-
} catch {
|
|
792
|
-
return { valid: false, error: "Invalid .ots proof format" };
|
|
793
|
-
}
|
|
794
|
-
if (originalDataHash !== void 0) {
|
|
795
|
-
let expected;
|
|
796
|
-
try {
|
|
797
|
-
expected = validateHash(originalDataHash);
|
|
798
|
-
} catch (err) {
|
|
799
|
-
return { valid: false, error: err instanceof Error ? err.message : "Invalid hash format" };
|
|
800
|
-
}
|
|
801
|
-
if (!bytesEq(expected, detached.fileDigest())) {
|
|
802
|
-
return { valid: false, error: "File hash does not match proof" };
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
const bitcoinAtts = detached.timestamp.allAttestations().filter(({ attestation }) => attestation.kind === "bitcoin");
|
|
806
|
-
if (bitcoinAtts.length === 0) {
|
|
807
|
-
const hasLitecoin = detached.timestamp.allAttestations().some(({ attestation }) => attestation.kind === "litecoin");
|
|
808
|
-
if (hasLitecoin) {
|
|
809
|
-
return { valid: false, error: "Litecoin verification is not supported by this client" };
|
|
810
|
-
}
|
|
811
|
-
return { valid: false, error: "No Bitcoin attestation found (timestamp not yet confirmed)" };
|
|
812
|
-
}
|
|
813
|
-
const explorer = new EsploraClient(networkLayer);
|
|
814
|
-
let lastError = "";
|
|
815
|
-
for (const { msg, attestation } of bitcoinAtts) {
|
|
816
|
-
if (attestation.kind !== "bitcoin") continue;
|
|
817
|
-
try {
|
|
818
|
-
const time = await verifyTimestampAttestation(Uint8Array.from(msg).reverse(), attestation, explorer, signal);
|
|
819
|
-
logger?.info(`Verified against Bitcoin block ${attestation.height}`);
|
|
820
|
-
return { valid: true, blockHeight: attestation.height, timestamp: time };
|
|
821
|
-
} catch (err) {
|
|
822
|
-
lastError = err instanceof Error ? err.message : String(err);
|
|
823
|
-
logger?.warn(`Bitcoin attestation at height ${attestation.height} failed: ${lastError}`);
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
return { valid: false, error: `Could not verify against the Bitcoin blockchain: ${lastError}` };
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// src/client.ts
|
|
830
|
-
var OpenTimestampsClient = class {
|
|
831
|
-
calendars;
|
|
832
|
-
networkLayer;
|
|
833
|
-
logger;
|
|
834
|
-
globalSignal;
|
|
835
|
-
minimumSuccessfulSubmissions;
|
|
836
|
-
/**
|
|
837
|
-
* Create a new OpenTimestamps client
|
|
838
|
-
*
|
|
839
|
-
* @param options Client configuration options
|
|
840
|
-
*/
|
|
841
|
-
constructor(options = {}) {
|
|
842
|
-
if (!options.calendars || options.calendars.length === 0) {
|
|
843
|
-
this.calendars = DEFAULT_CALENDARS;
|
|
844
|
-
this.logger?.info("No calendars provided, using defaults");
|
|
845
|
-
} else {
|
|
846
|
-
this.calendars = options.calendars;
|
|
847
|
-
}
|
|
848
|
-
this.minimumSuccessfulSubmissions = options.minimumSuccessfulSubmissions ?? 2;
|
|
849
|
-
const resilienceConfig = {
|
|
850
|
-
...DEFAULT_RESILIENCE,
|
|
851
|
-
...options.resilience,
|
|
852
|
-
retries: {
|
|
853
|
-
...DEFAULT_RESILIENCE.retries,
|
|
854
|
-
...options.resilience?.retries,
|
|
855
|
-
backoff: {
|
|
856
|
-
...DEFAULT_RESILIENCE.retries.backoff,
|
|
857
|
-
...options.resilience?.retries?.backoff
|
|
858
|
-
}
|
|
859
|
-
},
|
|
860
|
-
circuitBreaker: {
|
|
861
|
-
...DEFAULT_RESILIENCE.circuitBreaker,
|
|
862
|
-
...options.resilience?.circuitBreaker
|
|
863
|
-
}
|
|
864
|
-
};
|
|
865
|
-
this.logger = options.logger;
|
|
866
|
-
this.globalSignal = options.signal;
|
|
867
|
-
this.networkLayer = options.networkLayer ?? new ResilientNetworkLayer(resilienceConfig, this.logger);
|
|
868
|
-
this.logger?.info(`OpenTimestamps client initialized with ${this.calendars.length} calendars`);
|
|
869
|
-
}
|
|
870
|
-
/**
|
|
871
|
-
* Create a timestamp by submitting a hash to calendar servers
|
|
872
|
-
*
|
|
873
|
-
* @param hash SHA-256 hash of the data to timestamp (as Buffer or hex string)
|
|
874
|
-
* @param options Operation-specific options
|
|
875
|
-
* @returns Initial .ots proof with pending attestations
|
|
876
|
-
*
|
|
877
|
-
* @throws {ValidationError} If the hash is invalid
|
|
878
|
-
* @throws {StampError} If submission fails to all calendars
|
|
879
|
-
* @throws {NetworkError} If network errors occur
|
|
880
|
-
*
|
|
881
|
-
* @example
|
|
882
|
-
* ```typescript
|
|
883
|
-
* const hash = crypto.createHash('sha256').update('my data').digest()
|
|
884
|
-
* const otsProof = await client.stamp(hash)
|
|
885
|
-
* // Save otsProof to database as Buffer
|
|
886
|
-
* ```
|
|
887
|
-
*/
|
|
888
|
-
async stamp(hash, options) {
|
|
889
|
-
const signal = options?.signal || this.globalSignal;
|
|
890
|
-
return orchestrateStamp(
|
|
891
|
-
hash,
|
|
892
|
-
this.calendars,
|
|
893
|
-
this.networkLayer,
|
|
894
|
-
this.logger,
|
|
895
|
-
signal,
|
|
896
|
-
this.minimumSuccessfulSubmissions
|
|
897
|
-
);
|
|
898
|
-
}
|
|
899
|
-
/**
|
|
900
|
-
* Upgrade an incomplete timestamp proof by querying calendars for Bitcoin confirmation
|
|
901
|
-
*
|
|
902
|
-
* @param incompleteProof The initial .ots proof returned by stamp()
|
|
903
|
-
* @param options Operation-specific options
|
|
904
|
-
* @returns Upgraded .ots proof with Bitcoin attestation (if available)
|
|
905
|
-
*
|
|
906
|
-
* @throws {ValidationError} If the proof format is invalid
|
|
907
|
-
* @throws {UpgradeError} If no calendar has confirmed the timestamp yet
|
|
908
|
-
* @throws {NetworkError} If network errors occur
|
|
909
|
-
*
|
|
910
|
-
* @example
|
|
911
|
-
* ```typescript
|
|
912
|
-
* // Proof already has pending attestations from stamp()
|
|
913
|
-
* const upgradedProof = await client.upgrade(incompleteProof)
|
|
914
|
-
*
|
|
915
|
-
* // If upgrade throws UpgradeError, Bitcoin hasn't confirmed yet
|
|
916
|
-
* // Retry later (typically 10-60 minutes after stamp)
|
|
917
|
-
* ```
|
|
918
|
-
*/
|
|
919
|
-
async upgrade(incompleteProof, options) {
|
|
920
|
-
const signal = options?.signal || this.globalSignal;
|
|
921
|
-
return orchestrateUpgrade(
|
|
922
|
-
incompleteProof,
|
|
923
|
-
this.calendars,
|
|
924
|
-
this.networkLayer,
|
|
925
|
-
this.logger,
|
|
926
|
-
signal
|
|
927
|
-
);
|
|
928
|
-
}
|
|
929
|
-
/**
|
|
930
|
-
* Verify a complete timestamp proof against the Bitcoin blockchain
|
|
931
|
-
*
|
|
932
|
-
* @param proof The complete .ots proof with Bitcoin attestation
|
|
933
|
-
* @param originalDataHash Optional: the original data hash to verify against
|
|
934
|
-
* @returns Verification result with block details
|
|
935
|
-
*
|
|
936
|
-
* @example
|
|
937
|
-
* ```typescript
|
|
938
|
-
* const result = await client.verify(completeProof, originalHash)
|
|
939
|
-
*
|
|
940
|
-
* if (result.valid) {
|
|
941
|
-
* console.log(`Timestamp confirmed in Bitcoin block ${result.blockHeight}`)
|
|
942
|
-
* console.log(`Block timestamp: ${new Date(result.timestamp! * 1000)}`)
|
|
943
|
-
* } else {
|
|
944
|
-
* console.error(`Verification failed: ${result.error}`)
|
|
945
|
-
* }
|
|
946
|
-
* ```
|
|
947
|
-
*/
|
|
948
|
-
async verify(proof, originalDataHash) {
|
|
949
|
-
return orchestrateVerify(proof, this.networkLayer, originalDataHash, this.logger, this.globalSignal);
|
|
950
|
-
}
|
|
951
|
-
/**
|
|
952
|
-
* Get the current state of the circuit breaker for a calendar
|
|
953
|
-
* Useful for monitoring and debugging
|
|
954
|
-
*
|
|
955
|
-
* @param calendarUrl The calendar URL to check
|
|
956
|
-
* @returns Circuit state: 'CLOSED', 'OPEN', or 'HALF_OPEN' (undefined if not yet initialized)
|
|
957
|
-
*/
|
|
958
|
-
getCircuitState(calendarUrl) {
|
|
959
|
-
return this.networkLayer.getCircuitState(calendarUrl);
|
|
960
|
-
}
|
|
961
|
-
/**
|
|
962
|
-
* Reset the circuit breaker for a specific calendar
|
|
963
|
-
* Use this to manually recover a calendar that has been marked as failing
|
|
964
|
-
*
|
|
965
|
-
* @param calendarUrl The calendar URL to reset
|
|
966
|
-
*/
|
|
967
|
-
resetCircuit(calendarUrl) {
|
|
968
|
-
this.logger?.info(`Manually resetting circuit breaker for ${calendarUrl}`);
|
|
969
|
-
this.networkLayer.resetCircuit(calendarUrl);
|
|
970
|
-
}
|
|
971
|
-
/**
|
|
972
|
-
* Reset all circuit breakers
|
|
973
|
-
* Use this to clear all failure states
|
|
974
|
-
*/
|
|
975
|
-
resetAllCircuits() {
|
|
976
|
-
this.logger?.info("Manually resetting all circuit breakers");
|
|
977
|
-
this.networkLayer.resetAllCircuits();
|
|
978
|
-
}
|
|
979
|
-
};
|
|
980
|
-
|
|
981
|
-
// src/index.ts
|
|
982
|
-
var import_core4 = require("@otskit/core");
|
|
983
|
-
var import_core5 = require("@otskit/core");
|
|
984
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
985
|
-
0 && (module.exports = {
|
|
986
|
-
CalendarClient,
|
|
987
|
-
CalendarResponseTooLargeError,
|
|
988
|
-
CircuitBreakerError,
|
|
989
|
-
CircuitState,
|
|
990
|
-
CommitmentNotFoundError,
|
|
991
|
-
DEFAULT_AGGREGATORS,
|
|
992
|
-
DEFAULT_CALENDARS,
|
|
993
|
-
DEFAULT_CALENDAR_WHITELIST,
|
|
994
|
-
DEFAULT_RESILIENCE,
|
|
995
|
-
DetachedTimestampFile,
|
|
996
|
-
EsploraClient,
|
|
997
|
-
EsploraResponseError,
|
|
998
|
-
MAX_CALENDAR_RESPONSE_SIZE,
|
|
999
|
-
MAX_ESPLORA_RESPONSE_SIZE,
|
|
1000
|
-
NetworkError,
|
|
1001
|
-
OpenTimestampsClient,
|
|
1002
|
-
OpenTimestampsClientError,
|
|
1003
|
-
PUBLIC_ESPLORA_URL,
|
|
1004
|
-
ResilientNetworkLayer,
|
|
1005
|
-
StampError,
|
|
1006
|
-
Timestamp,
|
|
1007
|
-
UpgradeError,
|
|
1008
|
-
UrlWhitelist,
|
|
1009
|
-
ValidationError,
|
|
1010
|
-
verifyAgainstBlockheader,
|
|
1011
|
-
verifyTimestampAttestation
|
|
1012
|
-
});
|