@otskit/mcp 0.6.4 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -13
- package/dist/{chunk-4WQTV6CC.js → chunk-4S4MSBQL.js} +155 -118
- package/dist/chunk-KVHJHTFL.js +120 -0
- package/dist/chunk-XG7NDZR3.js +79 -0
- package/dist/{cli-2KSRIDHM.js → cli-WOUUWARK.js} +4 -4
- package/dist/index.js +7 -7
- package/dist/{server-P3TIJGLB.js → server-NDG4QZ56.js} +21 -18
- package/dist/watch-TQR2L42L.js +10 -0
- package/package.json +1 -1
- package/dist/chunk-Y3F7WBP6.js +0 -133
- package/dist/watch-MF5SD2H6.js +0 -41
package/README.md
CHANGED
|
@@ -8,16 +8,16 @@
|
|
|
8
8
|
[](https://www.npmjs.com/package/@otskit/mcp)
|
|
9
9
|
[](https://www.npmjs.com/package/@otskit/mcp)
|
|
10
10
|
[](https://www.typescriptlang.org/)
|
|
11
|
-
[](https://nodejs.org)
|
|
12
12
|
[](LICENSE)
|
|
13
13
|
[](https://glama.ai/mcp/servers/OTSkit/OTSkit-MCP)
|
|
14
|
-
[](https://smithery.ai/servers/otskit/otskit-mcp)
|
|
15
15
|
|
|
16
|
-
OpenTimestamps MCP server
|
|
16
|
+
OpenTimestamps MCP server - stamp, upgrade, and verify Bitcoin timestamps via AI agents.
|
|
17
17
|
|
|
18
|
-
Exposes a set of tools to any MCP-compatible agent so it can timestamp documents, monitor confirmation status, and verify proofs against the Bitcoin blockchain
|
|
18
|
+
Exposes a set of tools to any MCP-compatible agent so it can timestamp documents, monitor confirmation status, and verify proofs against the Bitcoin blockchain - all from a conversation.
|
|
19
19
|
|
|
20
|
-
> **Note on confirmation times:** After stamping, a proof is `pending` until Bitcoin confirms it. This typically takes
|
|
20
|
+
> **Note on confirmation times:** After stamping, a proof is `pending` until Bitcoin confirms it. This typically takes **~60 minutes** but can take **several hours** during network congestion. Use `ots-mcp watch` or `upgrade_timestamp` to monitor. A pending status is not an error.
|
|
21
21
|
|
|
22
22
|
## Install
|
|
23
23
|
|
|
@@ -44,7 +44,7 @@ Each command writes the MCP entry into the agent's config file, makes a `.bak` b
|
|
|
44
44
|
| `ots-mcp upgrade <id>` | Check if a pending stamp has been confirmed |
|
|
45
45
|
| `ots-mcp verify <id>` | Verify a stamp against Bitcoin |
|
|
46
46
|
| `ots-mcp list [status]` | List stamps (`pending` / `confirmed` / `failed`) |
|
|
47
|
-
| `ots-mcp watch [minutes]` |
|
|
47
|
+
| `ots-mcp watch [minutes]` | Monitor pending stamps and attempt due upgrades (default: 30 min, minimum: 15 min) |
|
|
48
48
|
| `ots-mcp check-pending` | Run one upgrade pass over all pending stamps |
|
|
49
49
|
| `ots-mcp scheduler install\|remove\|status` | Manage OS-level scheduler for auto-upgrades |
|
|
50
50
|
| `ots-mcp backup [dest]` | Backup the SQLite database |
|
|
@@ -56,10 +56,10 @@ Each command writes the MCP entry into the agent's config file, makes a `.bak` b
|
|
|
56
56
|
|---|---|
|
|
57
57
|
| `create_timestamp` | Stamp a SHA-256 hash against 4 public OTS calendars |
|
|
58
58
|
| `upgrade_timestamp` | Check if a pending stamp has been confirmed in Bitcoin |
|
|
59
|
-
| `verify_timestamp` | Verify a stamp
|
|
59
|
+
| `verify_timestamp` | Verify a stamp - proves hash existed before a given Bitcoin block |
|
|
60
60
|
| `inspect_timestamp` | Inspect a stored proof file without network calls |
|
|
61
61
|
| `list_pending` | List stamps with status, retry count, and filters |
|
|
62
|
-
| `watch` | Open a terminal window monitoring pending stamps
|
|
62
|
+
| `watch` | Open a terminal window monitoring pending stamps and attempting due upgrades |
|
|
63
63
|
| `hash_file` | Compute the SHA-256 of a local file and return it as a 64-char hex string (no network calls) |
|
|
64
64
|
| `stamp_file` | Compute SHA-256 of a local file and stamp it on Bitcoin in one step |
|
|
65
65
|
|
|
@@ -67,7 +67,7 @@ Each command writes the MCP entry into the agent's config file, makes a `.bak` b
|
|
|
67
67
|
|
|
68
68
|
All data is stored in `~/.ots-mcp/`:
|
|
69
69
|
|
|
70
|
-
```
|
|
70
|
+
```text
|
|
71
71
|
~/.ots-mcp/
|
|
72
72
|
ots-mcp.db # SQLite database (stamps, proof files)
|
|
73
73
|
config.json # Optional config overrides
|
|
@@ -104,7 +104,7 @@ npm test # run tests
|
|
|
104
104
|
|
|
105
105
|
## Dependencies
|
|
106
106
|
|
|
107
|
-
- [`@otskit/core`](https://github.com/AlexAlves87/otskit-core)
|
|
108
|
-
- [`@otskit/client`](https://github.com/AlexAlves87/otskit-client)
|
|
109
|
-
- [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
110
|
-
- `node-sqlite3-wasm`
|
|
107
|
+
- [`@otskit/core`](https://github.com/AlexAlves87/otskit-core) - OpenTimestamps core logic
|
|
108
|
+
- [`@otskit/client`](https://github.com/AlexAlves87/otskit-client) - OTS calendar client
|
|
109
|
+
- [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) - MCP SDK
|
|
110
|
+
- `node-sqlite3-wasm` - local database (pure WASM, no native compilation)
|
|
@@ -1,15 +1,138 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getDataDir
|
|
3
|
-
} from "./chunk-Y3F7WBP6.js";
|
|
4
1
|
import {
|
|
5
2
|
writeAtomic
|
|
6
3
|
} from "./chunk-YFSUDT24.js";
|
|
7
4
|
|
|
8
|
-
// src/
|
|
9
|
-
import { mkdirSync } from "fs";
|
|
5
|
+
// src/config.ts
|
|
6
|
+
import { readFileSync, existsSync, mkdirSync } from "fs";
|
|
10
7
|
import { join } from "path";
|
|
11
|
-
import {
|
|
12
|
-
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
function getDataDir() {
|
|
10
|
+
return process.env.OTS_MCP_DATA_DIR ?? join(homedir(), ".ots-mcp");
|
|
11
|
+
}
|
|
12
|
+
var DEFAULTS = {
|
|
13
|
+
stamp_enabled: true,
|
|
14
|
+
preserve_enabled: true,
|
|
15
|
+
preserve_whitelist: [],
|
|
16
|
+
preserve_max_bytes: 104857600,
|
|
17
|
+
preserve_max_files: 1e4,
|
|
18
|
+
scheduler_interval_minutes: 30,
|
|
19
|
+
calendar_timeout_ms: 1e4,
|
|
20
|
+
calendar_max_response_bytes: 1048576,
|
|
21
|
+
retry_max_attempts: 20,
|
|
22
|
+
log_file: join(getDataDir(), "ots-mcp.log"),
|
|
23
|
+
calendars: [
|
|
24
|
+
"https://alice.btc.calendar.opentimestamps.org",
|
|
25
|
+
"https://bob.btc.calendar.opentimestamps.org",
|
|
26
|
+
"https://finney.calendar.eternitywall.com",
|
|
27
|
+
"https://btc.calendar.catallaxy.com"
|
|
28
|
+
],
|
|
29
|
+
esplora_url: "https://blockstream.info/api"
|
|
30
|
+
};
|
|
31
|
+
function loadConfig() {
|
|
32
|
+
const dir = getDataDir();
|
|
33
|
+
mkdirSync(dir, { recursive: true });
|
|
34
|
+
const configPath = join(dir, "config.json");
|
|
35
|
+
if (!existsSync(configPath)) return { ...DEFAULTS };
|
|
36
|
+
const raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
37
|
+
return { ...DEFAULTS, ...raw };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/db/index.ts
|
|
41
|
+
import { createRequire } from "module";
|
|
42
|
+
import { join as join2 } from "path";
|
|
43
|
+
import { mkdirSync as mkdirSync2, statSync } from "fs";
|
|
44
|
+
|
|
45
|
+
// src/db/schema.ts
|
|
46
|
+
function initDb(db) {
|
|
47
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
48
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
49
|
+
runMigrations(db);
|
|
50
|
+
}
|
|
51
|
+
function runMigrations(db) {
|
|
52
|
+
const row = db.get("PRAGMA user_version");
|
|
53
|
+
if (row.user_version < 1) migrateTo1(db);
|
|
54
|
+
}
|
|
55
|
+
function migrateTo1(db) {
|
|
56
|
+
db.exec("BEGIN");
|
|
57
|
+
try {
|
|
58
|
+
db.exec(`
|
|
59
|
+
CREATE TABLE IF NOT EXISTS stamps (
|
|
60
|
+
id TEXT PRIMARY KEY,
|
|
61
|
+
hash TEXT NOT NULL,
|
|
62
|
+
status TEXT NOT NULL,
|
|
63
|
+
created_at TEXT NOT NULL,
|
|
64
|
+
confirmed_at TEXT,
|
|
65
|
+
bitcoin_block INTEGER,
|
|
66
|
+
bitcoin_time TEXT,
|
|
67
|
+
proof_path TEXT,
|
|
68
|
+
archive_path TEXT,
|
|
69
|
+
last_attempt_at TEXT,
|
|
70
|
+
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
71
|
+
last_error TEXT,
|
|
72
|
+
next_retry_at TEXT,
|
|
73
|
+
metadata TEXT
|
|
74
|
+
);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_stamps_hash ON stamps(hash);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_stamps_status ON stamps(status);
|
|
77
|
+
|
|
78
|
+
CREATE TABLE IF NOT EXISTS operations_log (
|
|
79
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
80
|
+
stamp_id TEXT NOT NULL REFERENCES stamps(id),
|
|
81
|
+
action TEXT NOT NULL,
|
|
82
|
+
result TEXT NOT NULL,
|
|
83
|
+
error_msg TEXT,
|
|
84
|
+
calendar_uri TEXT,
|
|
85
|
+
response_time_ms INTEGER,
|
|
86
|
+
created_at TEXT NOT NULL
|
|
87
|
+
);
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_oplog_stamp_id ON operations_log(stamp_id);
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_oplog_created ON operations_log(created_at);
|
|
90
|
+
`);
|
|
91
|
+
db.exec("PRAGMA user_version = 1");
|
|
92
|
+
db.exec("COMMIT");
|
|
93
|
+
} catch (e) {
|
|
94
|
+
db.exec("ROLLBACK");
|
|
95
|
+
throw e;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/db/index.ts
|
|
100
|
+
var _db = null;
|
|
101
|
+
function getDb() {
|
|
102
|
+
if (_db) return _db;
|
|
103
|
+
const _require = createRequire(import.meta.url);
|
|
104
|
+
const { Database } = _require("node-sqlite3-wasm");
|
|
105
|
+
const dir = getDataDir();
|
|
106
|
+
mkdirSync2(dir, { recursive: true });
|
|
107
|
+
_db = new Database(join2(dir, "db.sqlite"));
|
|
108
|
+
initDb(_db);
|
|
109
|
+
reconcileOrphans(_db);
|
|
110
|
+
return _db;
|
|
111
|
+
}
|
|
112
|
+
function backupDb(destPath) {
|
|
113
|
+
const escaped = destPath.replace(/'/g, "''");
|
|
114
|
+
getDb().exec(`VACUUM INTO '${escaped}'`);
|
|
115
|
+
}
|
|
116
|
+
function reconcileOrphans(db) {
|
|
117
|
+
const pending = db.all(
|
|
118
|
+
`SELECT id, proof_path FROM stamps WHERE status = 'pending' AND proof_path IS NOT NULL`
|
|
119
|
+
);
|
|
120
|
+
for (const row of pending) {
|
|
121
|
+
try {
|
|
122
|
+
statSync(row.proof_path);
|
|
123
|
+
} catch {
|
|
124
|
+
db.run(
|
|
125
|
+
`UPDATE stamps SET status = 'failed', last_error = ? WHERE id = ?`,
|
|
126
|
+
["proof file missing on disk", row.id]
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/tools/upgrade-timestamp.ts
|
|
133
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
134
|
+
import { OpenTimestampsClient, UpgradeError } from "@otskit/client";
|
|
135
|
+
import { DetachedTimestampFile } from "@otskit/core";
|
|
13
136
|
|
|
14
137
|
// src/db/stamps.ts
|
|
15
138
|
function insertStamp(db, params) {
|
|
@@ -86,50 +209,7 @@ function logOperation(db, params) {
|
|
|
86
209
|
);
|
|
87
210
|
}
|
|
88
211
|
|
|
89
|
-
// src/tools/create-timestamp.ts
|
|
90
|
-
var HEX64 = /^[0-9a-f]{64}$/i;
|
|
91
|
-
async function createTimestamp(input, db, config) {
|
|
92
|
-
if (!HEX64.test(input.hash)) {
|
|
93
|
-
return { error: "invalid_hash", details: "hash must be 64 hex characters (SHA-256)" };
|
|
94
|
-
}
|
|
95
|
-
const normalizedHash = input.hash.toLowerCase();
|
|
96
|
-
const client = new OpenTimestampsClient({
|
|
97
|
-
calendars: config.calendars,
|
|
98
|
-
resilience: { timeout: config.calendar_timeout_ms }
|
|
99
|
-
});
|
|
100
|
-
const t0 = Date.now();
|
|
101
|
-
let proofBuffer;
|
|
102
|
-
try {
|
|
103
|
-
proofBuffer = await client.stamp(normalizedHash);
|
|
104
|
-
} catch (e) {
|
|
105
|
-
return { error: "calendar_error", details: String(e) };
|
|
106
|
-
}
|
|
107
|
-
const responseTimeMs = Date.now() - t0;
|
|
108
|
-
const id = randomUUID();
|
|
109
|
-
const proofDir = join(getDataDir(), "proofs");
|
|
110
|
-
mkdirSync(proofDir, { recursive: true });
|
|
111
|
-
const proofPath = join(proofDir, `${id}.ots`);
|
|
112
|
-
try {
|
|
113
|
-
writeAtomic(proofPath, proofBuffer);
|
|
114
|
-
} catch (e) {
|
|
115
|
-
return { error: "storage_error", details: String(e) };
|
|
116
|
-
}
|
|
117
|
-
const record = insertStamp(db, { id, hash: normalizedHash, proof_path: proofPath });
|
|
118
|
-
logOperation(db, { stamp_id: id, action: "stamp", result: "success", response_time_ms: responseTimeMs });
|
|
119
|
-
return {
|
|
120
|
-
id: record.id,
|
|
121
|
-
hash: record.hash,
|
|
122
|
-
status: "pending",
|
|
123
|
-
calendars: config.calendars,
|
|
124
|
-
created_at: record.created_at,
|
|
125
|
-
proof_path: proofPath
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
212
|
// src/tools/upgrade-timestamp.ts
|
|
130
|
-
import { readFileSync } from "fs";
|
|
131
|
-
import { OpenTimestampsClient as OpenTimestampsClient2, UpgradeError } from "@otskit/client";
|
|
132
|
-
import { DetachedTimestampFile, StreamDeserializationContext } from "@otskit/core";
|
|
133
213
|
function collectAttestations(ts) {
|
|
134
214
|
const atts = [...ts.attestations];
|
|
135
215
|
for (const branch of ts.branches) {
|
|
@@ -139,8 +219,7 @@ function collectAttestations(ts) {
|
|
|
139
219
|
}
|
|
140
220
|
function checkBitcoinConfirmation(bytes) {
|
|
141
221
|
try {
|
|
142
|
-
const
|
|
143
|
-
const dtf = DetachedTimestampFile.deserialize(ctx);
|
|
222
|
+
const dtf = DetachedTimestampFile.deserialize(new Uint8Array(bytes));
|
|
144
223
|
const attestations = collectAttestations(dtf.timestamp);
|
|
145
224
|
const bitcoinAtts = attestations.filter((a) => a.kind === "bitcoin");
|
|
146
225
|
if (bitcoinAtts.length === 0) return { confirmed: false };
|
|
@@ -159,8 +238,8 @@ async function upgradeTimestamp(input, db, config) {
|
|
|
159
238
|
const record = getStamp(db, input.id);
|
|
160
239
|
if (!record) return { error: "not_found", details: `No stamp with id ${input.id}` };
|
|
161
240
|
if (!record.proof_path) return { error: "storage_error", details: "No proof_path on record" };
|
|
162
|
-
const proofBefore =
|
|
163
|
-
const client = new
|
|
241
|
+
const proofBefore = readFileSync2(record.proof_path);
|
|
242
|
+
const client = new OpenTimestampsClient({
|
|
164
243
|
calendars: config.calendars,
|
|
165
244
|
resilience: { timeout: config.calendar_timeout_ms }
|
|
166
245
|
});
|
|
@@ -172,6 +251,20 @@ async function upgradeTimestamp(input, db, config) {
|
|
|
172
251
|
upgraded = await client.upgrade(proofBefore);
|
|
173
252
|
} catch (e) {
|
|
174
253
|
if (e instanceof UpgradeError) {
|
|
254
|
+
const { confirmed: confirmed2, block: block2 } = checkBitcoinConfirmation(proofBefore);
|
|
255
|
+
if (confirmed2 && block2 !== void 0) {
|
|
256
|
+
const bitcoinTime = now;
|
|
257
|
+
updateStampStatus(db, input.id, {
|
|
258
|
+
status: "confirmed",
|
|
259
|
+
bitcoin_block: block2,
|
|
260
|
+
bitcoin_time: bitcoinTime,
|
|
261
|
+
confirmed_at: now,
|
|
262
|
+
last_attempt_at: now,
|
|
263
|
+
attempt_count: newAttemptCount
|
|
264
|
+
});
|
|
265
|
+
logOperation(db, { stamp_id: input.id, action: "upgrade", result: "success" });
|
|
266
|
+
return { id: input.id, status: "confirmed", bitcoin_block: block2, bitcoin_time: bitcoinTime, proof_path: record.proof_path };
|
|
267
|
+
}
|
|
175
268
|
updateStampStatus(db, input.id, { last_attempt_at: now, attempt_count: newAttemptCount, next_retry_at: next });
|
|
176
269
|
logOperation(db, { stamp_id: input.id, action: "upgrade", result: "pending" });
|
|
177
270
|
return { id: input.id, status: "pending", attempt_count: newAttemptCount, last_attempt_at: now, next_retry_at: next };
|
|
@@ -200,70 +293,14 @@ async function upgradeTimestamp(input, db, config) {
|
|
|
200
293
|
return { id: input.id, status: "pending", attempt_count: newAttemptCount, last_attempt_at: now, next_retry_at: next };
|
|
201
294
|
}
|
|
202
295
|
|
|
203
|
-
// src/tools/verify-timestamp.ts
|
|
204
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
205
|
-
import { OpenTimestampsClient as OpenTimestampsClient3 } from "@otskit/client";
|
|
206
|
-
async function verifyTimestamp(input, db, config) {
|
|
207
|
-
const record = getStamp(db, input.id);
|
|
208
|
-
if (!record) return { error: "not_found", details: `No stamp with id ${input.id}` };
|
|
209
|
-
if (!record.proof_path) return { error: "storage_error", details: "No proof_path on record" };
|
|
210
|
-
let proofBytes;
|
|
211
|
-
try {
|
|
212
|
-
proofBytes = readFileSync2(record.proof_path);
|
|
213
|
-
} catch (e) {
|
|
214
|
-
return { error: "storage_error", details: String(e) };
|
|
215
|
-
}
|
|
216
|
-
const client = new OpenTimestampsClient3({
|
|
217
|
-
calendars: config.calendars,
|
|
218
|
-
resilience: { timeout: config.calendar_timeout_ms }
|
|
219
|
-
});
|
|
220
|
-
let result;
|
|
221
|
-
try {
|
|
222
|
-
result = await client.verify(proofBytes, record.hash);
|
|
223
|
-
} catch (e) {
|
|
224
|
-
logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: String(e) });
|
|
225
|
-
return { status: "network_error", hash: record.hash, details: String(e) };
|
|
226
|
-
}
|
|
227
|
-
if (!result.valid) {
|
|
228
|
-
if (result.error?.includes("No Bitcoin attestation")) {
|
|
229
|
-
logOperation(db, { stamp_id: input.id, action: "verify", result: "pending" });
|
|
230
|
-
return { status: "pending", hash: record.hash, calendars: config.calendars };
|
|
231
|
-
}
|
|
232
|
-
if (result.error?.toLowerCase().includes("invalid") || result.error?.toLowerCase().includes("corrupt")) {
|
|
233
|
-
logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
|
|
234
|
-
return { status: "invalid", hash: record.hash, reason: result.error ?? "unknown" };
|
|
235
|
-
}
|
|
236
|
-
logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
|
|
237
|
-
return { status: "unknown", hash: record.hash };
|
|
238
|
-
}
|
|
239
|
-
logOperation(db, { stamp_id: input.id, action: "verify", result: "success" });
|
|
240
|
-
return {
|
|
241
|
-
status: "confirmed",
|
|
242
|
-
hash: record.hash,
|
|
243
|
-
bitcoin_block: result.blockHeight,
|
|
244
|
-
bitcoin_time: new Date(result.timestamp * 1e3).toISOString()
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// src/tools/list-pending.ts
|
|
249
|
-
function toPublic({ attempt_count: _, last_attempt_at: __, next_retry_at: ___, ...rest }) {
|
|
250
|
-
return rest;
|
|
251
|
-
}
|
|
252
|
-
function listPending(input, db, _config) {
|
|
253
|
-
const result = listStamps(db, {
|
|
254
|
-
status: input.status ?? "pending",
|
|
255
|
-
limit: Math.min(input.limit ?? 50, 200),
|
|
256
|
-
offset: input.offset ?? 0,
|
|
257
|
-
older_than_hours: input.older_than_hours,
|
|
258
|
-
due_now: input.due_now
|
|
259
|
-
});
|
|
260
|
-
return { items: result.items.map(toPublic), total: result.total };
|
|
261
|
-
}
|
|
262
|
-
|
|
263
296
|
export {
|
|
297
|
+
getDataDir,
|
|
298
|
+
loadConfig,
|
|
299
|
+
getDb,
|
|
300
|
+
backupDb,
|
|
301
|
+
insertStamp,
|
|
264
302
|
getStamp,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
listPending
|
|
303
|
+
listStamps,
|
|
304
|
+
logOperation,
|
|
305
|
+
upgradeTimestamp
|
|
269
306
|
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDataDir,
|
|
3
|
+
getStamp,
|
|
4
|
+
insertStamp,
|
|
5
|
+
listStamps,
|
|
6
|
+
logOperation
|
|
7
|
+
} from "./chunk-4S4MSBQL.js";
|
|
8
|
+
import {
|
|
9
|
+
writeAtomic
|
|
10
|
+
} from "./chunk-YFSUDT24.js";
|
|
11
|
+
|
|
12
|
+
// src/tools/create-timestamp.ts
|
|
13
|
+
import { mkdirSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { randomUUID } from "crypto";
|
|
16
|
+
import { OpenTimestampsClient } from "@otskit/client";
|
|
17
|
+
var HEX64 = /^[0-9a-f]{64}$/i;
|
|
18
|
+
async function createTimestamp(input, db, config) {
|
|
19
|
+
if (!HEX64.test(input.hash)) {
|
|
20
|
+
return { error: "invalid_hash", details: "hash must be 64 hex characters (SHA-256)" };
|
|
21
|
+
}
|
|
22
|
+
const normalizedHash = input.hash.toLowerCase();
|
|
23
|
+
const client = new OpenTimestampsClient({
|
|
24
|
+
calendars: config.calendars,
|
|
25
|
+
resilience: { timeout: config.calendar_timeout_ms }
|
|
26
|
+
});
|
|
27
|
+
const t0 = Date.now();
|
|
28
|
+
let proofBuffer;
|
|
29
|
+
try {
|
|
30
|
+
proofBuffer = await client.stamp(normalizedHash);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return { error: "calendar_error", details: String(e) };
|
|
33
|
+
}
|
|
34
|
+
const responseTimeMs = Date.now() - t0;
|
|
35
|
+
const id = randomUUID();
|
|
36
|
+
const proofDir = join(getDataDir(), "proofs");
|
|
37
|
+
mkdirSync(proofDir, { recursive: true });
|
|
38
|
+
const proofPath = join(proofDir, `${id}.ots`);
|
|
39
|
+
try {
|
|
40
|
+
writeAtomic(proofPath, proofBuffer);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return { error: "storage_error", details: String(e) };
|
|
43
|
+
}
|
|
44
|
+
const record = insertStamp(db, { id, hash: normalizedHash, proof_path: proofPath });
|
|
45
|
+
logOperation(db, { stamp_id: id, action: "stamp", result: "success", response_time_ms: responseTimeMs });
|
|
46
|
+
return {
|
|
47
|
+
id: record.id,
|
|
48
|
+
hash: record.hash,
|
|
49
|
+
status: "pending",
|
|
50
|
+
calendars: config.calendars,
|
|
51
|
+
created_at: record.created_at,
|
|
52
|
+
proof_path: proofPath
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/tools/verify-timestamp.ts
|
|
57
|
+
import { readFileSync } from "fs";
|
|
58
|
+
import { OpenTimestampsClient as OpenTimestampsClient2 } from "@otskit/client";
|
|
59
|
+
async function verifyTimestamp(input, db, config) {
|
|
60
|
+
const record = getStamp(db, input.id);
|
|
61
|
+
if (!record) return { error: "not_found", details: `No stamp with id ${input.id}` };
|
|
62
|
+
if (!record.proof_path) return { error: "storage_error", details: "No proof_path on record" };
|
|
63
|
+
let proofBytes;
|
|
64
|
+
try {
|
|
65
|
+
proofBytes = readFileSync(record.proof_path);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return { error: "storage_error", details: String(e) };
|
|
68
|
+
}
|
|
69
|
+
const client = new OpenTimestampsClient2({
|
|
70
|
+
calendars: config.calendars,
|
|
71
|
+
resilience: { timeout: config.calendar_timeout_ms }
|
|
72
|
+
});
|
|
73
|
+
let result;
|
|
74
|
+
try {
|
|
75
|
+
result = await client.verify(proofBytes, record.hash);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: String(e) });
|
|
78
|
+
return { status: "network_error", hash: record.hash, details: String(e) };
|
|
79
|
+
}
|
|
80
|
+
if (!result.valid) {
|
|
81
|
+
if (result.error?.includes("No Bitcoin attestation")) {
|
|
82
|
+
logOperation(db, { stamp_id: input.id, action: "verify", result: "pending" });
|
|
83
|
+
return { status: "pending", hash: record.hash, calendars: config.calendars };
|
|
84
|
+
}
|
|
85
|
+
if (result.error?.toLowerCase().includes("invalid") || result.error?.toLowerCase().includes("corrupt")) {
|
|
86
|
+
logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
|
|
87
|
+
return { status: "invalid", hash: record.hash, reason: result.error ?? "unknown" };
|
|
88
|
+
}
|
|
89
|
+
logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
|
|
90
|
+
return { status: "unknown", hash: record.hash };
|
|
91
|
+
}
|
|
92
|
+
logOperation(db, { stamp_id: input.id, action: "verify", result: "success" });
|
|
93
|
+
return {
|
|
94
|
+
status: "confirmed",
|
|
95
|
+
hash: record.hash,
|
|
96
|
+
bitcoin_block: result.blockHeight,
|
|
97
|
+
bitcoin_time: new Date(result.timestamp * 1e3).toISOString()
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/tools/list-pending.ts
|
|
102
|
+
function toPublic({ attempt_count: _, last_attempt_at: __, next_retry_at: ___, ...rest }) {
|
|
103
|
+
return rest;
|
|
104
|
+
}
|
|
105
|
+
function listPending(input, db, _config) {
|
|
106
|
+
const result = listStamps(db, {
|
|
107
|
+
status: input.status ?? "pending",
|
|
108
|
+
limit: Math.min(input.limit ?? 50, 200),
|
|
109
|
+
offset: input.offset ?? 0,
|
|
110
|
+
older_than_hours: input.older_than_hours,
|
|
111
|
+
due_now: input.due_now
|
|
112
|
+
});
|
|
113
|
+
return { items: result.items.map(toPublic), total: result.total };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export {
|
|
117
|
+
createTimestamp,
|
|
118
|
+
verifyTimestamp,
|
|
119
|
+
listPending
|
|
120
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDb,
|
|
3
|
+
loadConfig,
|
|
4
|
+
upgradeTimestamp
|
|
5
|
+
} from "./chunk-4S4MSBQL.js";
|
|
6
|
+
|
|
7
|
+
// src/tools/watch.ts
|
|
8
|
+
var DEFAULT_WATCH_INTERVAL_MINUTES = 30;
|
|
9
|
+
var MIN_WATCH_INTERVAL_MINUTES = 15;
|
|
10
|
+
var MAX_UPGRADES_PER_TICK = 20;
|
|
11
|
+
function normalizeWatchInterval(intervalMinutes = DEFAULT_WATCH_INTERVAL_MINUTES) {
|
|
12
|
+
if (!Number.isFinite(intervalMinutes)) return DEFAULT_WATCH_INTERVAL_MINUTES;
|
|
13
|
+
return Math.max(MIN_WATCH_INTERVAL_MINUTES, Math.floor(intervalMinutes));
|
|
14
|
+
}
|
|
15
|
+
async function watchPending(intervalMinutes = DEFAULT_WATCH_INTERVAL_MINUTES) {
|
|
16
|
+
const config = loadConfig();
|
|
17
|
+
const db = getDb();
|
|
18
|
+
const minutes = normalizeWatchInterval(intervalMinutes);
|
|
19
|
+
process.stdout.write(`Watching pending stamps and upgrading due proofs every ${minutes} min. Ctrl+C to stop.
|
|
20
|
+
|
|
21
|
+
`);
|
|
22
|
+
async function tick() {
|
|
23
|
+
const dueRows = db.all(
|
|
24
|
+
`SELECT id FROM stamps
|
|
25
|
+
WHERE status = 'pending'
|
|
26
|
+
AND (next_retry_at IS NULL OR next_retry_at <= ?)
|
|
27
|
+
ORDER BY created_at ASC
|
|
28
|
+
LIMIT ?`,
|
|
29
|
+
[(/* @__PURE__ */ new Date()).toISOString(), MAX_UPGRADES_PER_TICK]
|
|
30
|
+
);
|
|
31
|
+
if (dueRows.length > 0) {
|
|
32
|
+
process.stdout.write(`${now()} - upgrading ${dueRows.length} due stamp(s)
|
|
33
|
+
`);
|
|
34
|
+
}
|
|
35
|
+
for (const row of dueRows) {
|
|
36
|
+
const result = await upgradeTimestamp({ id: row.id }, db, config);
|
|
37
|
+
const status = "status" in result ? result.status : `error:${result.error}`;
|
|
38
|
+
process.stdout.write(` upgrade ${row.id.slice(0, 8)} -> ${status}
|
|
39
|
+
`);
|
|
40
|
+
}
|
|
41
|
+
const rows = db.all(
|
|
42
|
+
`SELECT id, hash, status, attempt_count, bitcoin_block, confirmed_at, next_retry_at
|
|
43
|
+
FROM stamps
|
|
44
|
+
WHERE status != 'confirmed'
|
|
45
|
+
ORDER BY created_at DESC`
|
|
46
|
+
);
|
|
47
|
+
const confirmed = db.get(`SELECT COUNT(*) as n FROM stamps WHERE status = 'confirmed'`);
|
|
48
|
+
process.stdout.write(`${now()} - ${rows.length} pending, ${confirmed.n} confirmed
|
|
49
|
+
`);
|
|
50
|
+
for (const row of rows) {
|
|
51
|
+
const next = row.next_retry_at ? ` next ${row.next_retry_at.replace("T", " ").slice(0, 19)}` : "";
|
|
52
|
+
process.stdout.write(` ${row.id.slice(0, 8)} ${row.status} (${row.attempt_count} attempts)${next}
|
|
53
|
+
`);
|
|
54
|
+
}
|
|
55
|
+
if (rows.length === 0) {
|
|
56
|
+
process.stdout.write(` (no pending stamps)
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
process.stdout.write("\n");
|
|
60
|
+
}
|
|
61
|
+
function now() {
|
|
62
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
63
|
+
}
|
|
64
|
+
async function loop() {
|
|
65
|
+
try {
|
|
66
|
+
await tick();
|
|
67
|
+
} catch (e) {
|
|
68
|
+
process.stderr.write(`watch error: ${String(e)}
|
|
69
|
+
`);
|
|
70
|
+
}
|
|
71
|
+
setTimeout(loop, minutes * 60 * 1e3);
|
|
72
|
+
}
|
|
73
|
+
await loop();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export {
|
|
77
|
+
normalizeWatchInterval,
|
|
78
|
+
watchPending
|
|
79
|
+
};
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createTimestamp,
|
|
3
3
|
listPending,
|
|
4
|
-
upgradeTimestamp,
|
|
5
4
|
verifyTimestamp
|
|
6
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-KVHJHTFL.js";
|
|
7
6
|
import {
|
|
8
7
|
backupDb,
|
|
9
8
|
getDb,
|
|
10
|
-
loadConfig
|
|
11
|
-
|
|
9
|
+
loadConfig,
|
|
10
|
+
upgradeTimestamp
|
|
11
|
+
} from "./chunk-4S4MSBQL.js";
|
|
12
12
|
import "./chunk-YFSUDT24.js";
|
|
13
13
|
|
|
14
14
|
// src/cli.ts
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ if (!command || command === "--help" || command === "help") {
|
|
|
7
7
|
Commands:
|
|
8
8
|
serve Start MCP server (stdio transport)
|
|
9
9
|
setup <target> Configure MCP for an agent (claude | claude-code | codex)
|
|
10
|
-
watch [interval] Watch pending stamps
|
|
10
|
+
watch [interval] Watch and upgrade due pending stamps (default: 30 min, minimum: 15 min)
|
|
11
11
|
stamp <hash> Stamp a SHA-256 hash
|
|
12
12
|
upgrade <id> Upgrade a pending stamp
|
|
13
13
|
verify <id> Verify a stamp
|
|
@@ -20,7 +20,7 @@ Commands:
|
|
|
20
20
|
}
|
|
21
21
|
switch (command) {
|
|
22
22
|
case "serve": {
|
|
23
|
-
const { runServer } = await import("./server-
|
|
23
|
+
const { runServer } = await import("./server-NDG4QZ56.js");
|
|
24
24
|
await runServer();
|
|
25
25
|
break;
|
|
26
26
|
}
|
|
@@ -49,11 +49,11 @@ switch (command) {
|
|
|
49
49
|
break;
|
|
50
50
|
}
|
|
51
51
|
case "watch": {
|
|
52
|
-
const { watchPending } = await import("./watch-
|
|
52
|
+
const { normalizeWatchInterval, watchPending } = await import("./watch-TQR2L42L.js");
|
|
53
53
|
const parsed = args[0] ? parseInt(args[0], 10) : NaN;
|
|
54
|
-
const interval = isNaN(parsed)
|
|
55
|
-
if (args[0] && (isNaN(parsed) || parsed <
|
|
56
|
-
process.stderr.write(`
|
|
54
|
+
const interval = normalizeWatchInterval(isNaN(parsed) ? void 0 : parsed);
|
|
55
|
+
if (args[0] && (isNaN(parsed) || parsed < 15))
|
|
56
|
+
process.stderr.write(`Invalid interval "${args[0]}", using ${interval} min
|
|
57
57
|
`);
|
|
58
58
|
await watchPending(interval);
|
|
59
59
|
break;
|
|
@@ -65,7 +65,7 @@ switch (command) {
|
|
|
65
65
|
case "check-pending":
|
|
66
66
|
case "backup":
|
|
67
67
|
case "scheduler": {
|
|
68
|
-
const { runCli } = await import("./cli-
|
|
68
|
+
const { runCli } = await import("./cli-WOUUWARK.js");
|
|
69
69
|
await runCli(command, args);
|
|
70
70
|
break;
|
|
71
71
|
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createTimestamp,
|
|
3
|
-
getStamp,
|
|
4
3
|
listPending,
|
|
5
|
-
upgradeTimestamp,
|
|
6
4
|
verifyTimestamp
|
|
7
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-KVHJHTFL.js";
|
|
6
|
+
import {
|
|
7
|
+
normalizeWatchInterval
|
|
8
|
+
} from "./chunk-XG7NDZR3.js";
|
|
8
9
|
import {
|
|
9
10
|
getDb,
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
getStamp,
|
|
12
|
+
loadConfig,
|
|
13
|
+
upgradeTimestamp
|
|
14
|
+
} from "./chunk-4S4MSBQL.js";
|
|
12
15
|
import "./chunk-YFSUDT24.js";
|
|
13
16
|
|
|
14
17
|
// src/server.ts
|
|
@@ -61,8 +64,8 @@ function inspectTimestamp(input, db, _config) {
|
|
|
61
64
|
|
|
62
65
|
// src/tools/watch-window.ts
|
|
63
66
|
import { exec } from "child_process";
|
|
64
|
-
function openWatchWindow(intervalMinutes
|
|
65
|
-
const minutes =
|
|
67
|
+
function openWatchWindow(intervalMinutes) {
|
|
68
|
+
const minutes = normalizeWatchInterval(intervalMinutes);
|
|
66
69
|
const cmd = `start powershell.exe -NoExit -Command "ots-mcp watch ${minutes}"`;
|
|
67
70
|
let errorMsg;
|
|
68
71
|
exec(cmd, { shell: "cmd" }, (err) => {
|
|
@@ -98,7 +101,7 @@ async function hashFileTool(input) {
|
|
|
98
101
|
var TOOL_DEFINITIONS = [
|
|
99
102
|
{
|
|
100
103
|
name: "create_timestamp",
|
|
101
|
-
description: "
|
|
104
|
+
description: "Creates a verifiable Bitcoin timestamp for a SHA-256 hash using the OpenTimestamps protocol. Submits the hash to four public OTS calendars (alice.btc, bob.btc, finney, catallaxy) and stores a pending proof locally. Returns a stamp ID to track confirmation status. Confirmation typically takes ~60 minutes but can take several hours during network congestion.",
|
|
102
105
|
inputSchema: {
|
|
103
106
|
type: "object",
|
|
104
107
|
properties: { hash: { type: "string", description: "SHA-256 hex digest (64 chars)" } },
|
|
@@ -113,7 +116,7 @@ var TOOL_DEFINITIONS = [
|
|
|
113
116
|
},
|
|
114
117
|
{
|
|
115
118
|
name: "upgrade_timestamp",
|
|
116
|
-
description: "
|
|
119
|
+
description: "Attempts to upgrade a pending OpenTimestamps proof by fetching the latest merkle tree from the calendars. If Bitcoin has included the timestamp, the proof becomes confirmed and the bitcoin_block is recorded. Safe to call repeatedly \u2014 if not yet confirmed, it schedules the next retry automatically.",
|
|
117
120
|
inputSchema: {
|
|
118
121
|
type: "object",
|
|
119
122
|
properties: { id: { type: "string", description: "UUID from the stamp record" } },
|
|
@@ -128,7 +131,7 @@ var TOOL_DEFINITIONS = [
|
|
|
128
131
|
},
|
|
129
132
|
{
|
|
130
133
|
name: "verify_timestamp",
|
|
131
|
-
description: "Verifies a
|
|
134
|
+
description: "Verifies a timestamp proof against the Bitcoin blockchain via an Esplora API. Proves that a specific hash existed before a given Bitcoin block height. Does NOT affirm document authorship, content truth, or legal validity \u2014 it only provides a cryptographic proof of existence at a point in time.",
|
|
132
135
|
inputSchema: {
|
|
133
136
|
type: "object",
|
|
134
137
|
properties: { id: { type: "string", description: "UUID from the stamp record" } },
|
|
@@ -143,7 +146,7 @@ var TOOL_DEFINITIONS = [
|
|
|
143
146
|
},
|
|
144
147
|
{
|
|
145
148
|
name: "inspect_timestamp",
|
|
146
|
-
description: "
|
|
149
|
+
description: "Reads a stored proof file from disk without any network calls. Returns proof metadata including size, number of calendar attestations (pending promises from OTS servers) and Bitcoin attestations (actual confirmed blocks). A stamp is only truly confirmed when bitcoin_attestations > 0 and bitcoin_confirmed is true \u2014 calendar_attestations alone do not prove Bitcoin confirmation.",
|
|
147
150
|
inputSchema: {
|
|
148
151
|
type: "object",
|
|
149
152
|
properties: { id: { type: "string", description: "UUID from the stamp record" } },
|
|
@@ -158,7 +161,7 @@ var TOOL_DEFINITIONS = [
|
|
|
158
161
|
},
|
|
159
162
|
{
|
|
160
163
|
name: "list_pending",
|
|
161
|
-
description: "Lists stamp records with status and
|
|
164
|
+
description: "Lists stamp records from the local database with their current status, retry count, and next scheduled upgrade time. Filter by status (pending, confirmed, failed), page through results, or find stamps older than N hours. Use this to monitor the state of all timestamped hashes.",
|
|
162
165
|
inputSchema: {
|
|
163
166
|
type: "object",
|
|
164
167
|
properties: {
|
|
@@ -177,7 +180,7 @@ var TOOL_DEFINITIONS = [
|
|
|
177
180
|
},
|
|
178
181
|
{
|
|
179
182
|
name: "hash_file",
|
|
180
|
-
description: "Computes the SHA-256 hash of a local file and returns it as a 64-character hex string.
|
|
183
|
+
description: "Computes the SHA-256 hash of a local file and returns it as a 64-character hex string. Purely local \u2014 no network calls, no data stored. Use this to get the hash before calling create_timestamp, or to verify the integrity of a file independently.",
|
|
181
184
|
inputSchema: {
|
|
182
185
|
type: "object",
|
|
183
186
|
properties: { path: { type: "string", description: "Absolute path to the file" } },
|
|
@@ -192,7 +195,7 @@ var TOOL_DEFINITIONS = [
|
|
|
192
195
|
},
|
|
193
196
|
{
|
|
194
197
|
name: "stamp_file",
|
|
195
|
-
description: "
|
|
198
|
+
description: "Convenience tool that hashes a local file and stamps it on Bitcoin in one step. Computes the SHA-256 of the file, then submits it to four public OTS calendars. The file contents are never sent externally \u2014 only the hash is. Returns a stamp ID for tracking confirmation.",
|
|
196
199
|
inputSchema: {
|
|
197
200
|
type: "object",
|
|
198
201
|
properties: { path: { type: "string", description: "Absolute path to the file to stamp" } },
|
|
@@ -207,18 +210,18 @@ var TOOL_DEFINITIONS = [
|
|
|
207
210
|
},
|
|
208
211
|
{
|
|
209
212
|
name: "watch",
|
|
210
|
-
description: "Opens a new terminal window that monitors pending stamps
|
|
213
|
+
description: "Opens a new terminal window that continuously monitors pending stamps and attempts due upgrades at each interval. Useful for long-running monitoring sessions after stamping. The window remains open so the user can watch confirmation progress in real time. Minimum interval is 15 minutes to avoid hammering OTS calendars.",
|
|
211
214
|
inputSchema: {
|
|
212
215
|
type: "object",
|
|
213
216
|
properties: {
|
|
214
|
-
interval_minutes: { type: "number", description: "Polling interval in minutes (default:
|
|
217
|
+
interval_minutes: { type: "number", description: "Polling interval in minutes (default: 30, minimum: 15)" }
|
|
215
218
|
}
|
|
216
219
|
},
|
|
217
220
|
annotations: {
|
|
218
|
-
readOnlyHint:
|
|
221
|
+
readOnlyHint: false,
|
|
219
222
|
destructiveHint: false,
|
|
220
223
|
idempotentHint: false,
|
|
221
|
-
openWorldHint:
|
|
224
|
+
openWorldHint: true
|
|
222
225
|
}
|
|
223
226
|
}
|
|
224
227
|
];
|
package/package.json
CHANGED
package/dist/chunk-Y3F7WBP6.js
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
// src/config.ts
|
|
2
|
-
import { readFileSync, existsSync, mkdirSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { homedir } from "os";
|
|
5
|
-
function getDataDir() {
|
|
6
|
-
return process.env.OTS_MCP_DATA_DIR ?? join(homedir(), ".ots-mcp");
|
|
7
|
-
}
|
|
8
|
-
var DEFAULTS = {
|
|
9
|
-
stamp_enabled: true,
|
|
10
|
-
preserve_enabled: true,
|
|
11
|
-
preserve_whitelist: [],
|
|
12
|
-
preserve_max_bytes: 104857600,
|
|
13
|
-
preserve_max_files: 1e4,
|
|
14
|
-
scheduler_interval_minutes: 30,
|
|
15
|
-
calendar_timeout_ms: 1e4,
|
|
16
|
-
calendar_max_response_bytes: 1048576,
|
|
17
|
-
retry_max_attempts: 20,
|
|
18
|
-
log_file: join(getDataDir(), "ots-mcp.log"),
|
|
19
|
-
calendars: [
|
|
20
|
-
"https://alice.btc.calendar.opentimestamps.org",
|
|
21
|
-
"https://bob.btc.calendar.opentimestamps.org",
|
|
22
|
-
"https://finney.calendar.eternitywall.com",
|
|
23
|
-
"https://btc.calendar.catallaxy.com"
|
|
24
|
-
],
|
|
25
|
-
esplora_url: "https://blockstream.info/api"
|
|
26
|
-
};
|
|
27
|
-
function loadConfig() {
|
|
28
|
-
const dir = getDataDir();
|
|
29
|
-
mkdirSync(dir, { recursive: true });
|
|
30
|
-
const configPath = join(dir, "config.json");
|
|
31
|
-
if (!existsSync(configPath)) return { ...DEFAULTS };
|
|
32
|
-
const raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
33
|
-
return { ...DEFAULTS, ...raw };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// src/db/index.ts
|
|
37
|
-
import { createRequire } from "module";
|
|
38
|
-
import { join as join2 } from "path";
|
|
39
|
-
import { mkdirSync as mkdirSync2, statSync } from "fs";
|
|
40
|
-
|
|
41
|
-
// src/db/schema.ts
|
|
42
|
-
function initDb(db) {
|
|
43
|
-
db.exec("PRAGMA busy_timeout = 5000");
|
|
44
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
45
|
-
runMigrations(db);
|
|
46
|
-
}
|
|
47
|
-
function runMigrations(db) {
|
|
48
|
-
const row = db.get("PRAGMA user_version");
|
|
49
|
-
if (row.user_version < 1) migrateTo1(db);
|
|
50
|
-
}
|
|
51
|
-
function migrateTo1(db) {
|
|
52
|
-
db.exec("BEGIN");
|
|
53
|
-
try {
|
|
54
|
-
db.exec(`
|
|
55
|
-
CREATE TABLE IF NOT EXISTS stamps (
|
|
56
|
-
id TEXT PRIMARY KEY,
|
|
57
|
-
hash TEXT NOT NULL,
|
|
58
|
-
status TEXT NOT NULL,
|
|
59
|
-
created_at TEXT NOT NULL,
|
|
60
|
-
confirmed_at TEXT,
|
|
61
|
-
bitcoin_block INTEGER,
|
|
62
|
-
bitcoin_time TEXT,
|
|
63
|
-
proof_path TEXT,
|
|
64
|
-
archive_path TEXT,
|
|
65
|
-
last_attempt_at TEXT,
|
|
66
|
-
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
67
|
-
last_error TEXT,
|
|
68
|
-
next_retry_at TEXT,
|
|
69
|
-
metadata TEXT
|
|
70
|
-
);
|
|
71
|
-
CREATE INDEX IF NOT EXISTS idx_stamps_hash ON stamps(hash);
|
|
72
|
-
CREATE INDEX IF NOT EXISTS idx_stamps_status ON stamps(status);
|
|
73
|
-
|
|
74
|
-
CREATE TABLE IF NOT EXISTS operations_log (
|
|
75
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
76
|
-
stamp_id TEXT NOT NULL REFERENCES stamps(id),
|
|
77
|
-
action TEXT NOT NULL,
|
|
78
|
-
result TEXT NOT NULL,
|
|
79
|
-
error_msg TEXT,
|
|
80
|
-
calendar_uri TEXT,
|
|
81
|
-
response_time_ms INTEGER,
|
|
82
|
-
created_at TEXT NOT NULL
|
|
83
|
-
);
|
|
84
|
-
CREATE INDEX IF NOT EXISTS idx_oplog_stamp_id ON operations_log(stamp_id);
|
|
85
|
-
CREATE INDEX IF NOT EXISTS idx_oplog_created ON operations_log(created_at);
|
|
86
|
-
`);
|
|
87
|
-
db.exec("PRAGMA user_version = 1");
|
|
88
|
-
db.exec("COMMIT");
|
|
89
|
-
} catch (e) {
|
|
90
|
-
db.exec("ROLLBACK");
|
|
91
|
-
throw e;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// src/db/index.ts
|
|
96
|
-
var _db = null;
|
|
97
|
-
function getDb() {
|
|
98
|
-
if (_db) return _db;
|
|
99
|
-
const _require = createRequire(import.meta.url);
|
|
100
|
-
const { Database } = _require("node-sqlite3-wasm");
|
|
101
|
-
const dir = getDataDir();
|
|
102
|
-
mkdirSync2(dir, { recursive: true });
|
|
103
|
-
_db = new Database(join2(dir, "db.sqlite"));
|
|
104
|
-
initDb(_db);
|
|
105
|
-
reconcileOrphans(_db);
|
|
106
|
-
return _db;
|
|
107
|
-
}
|
|
108
|
-
function backupDb(destPath) {
|
|
109
|
-
const escaped = destPath.replace(/'/g, "''");
|
|
110
|
-
getDb().exec(`VACUUM INTO '${escaped}'`);
|
|
111
|
-
}
|
|
112
|
-
function reconcileOrphans(db) {
|
|
113
|
-
const pending = db.all(
|
|
114
|
-
`SELECT id, proof_path FROM stamps WHERE status = 'pending' AND proof_path IS NOT NULL`
|
|
115
|
-
);
|
|
116
|
-
for (const row of pending) {
|
|
117
|
-
try {
|
|
118
|
-
statSync(row.proof_path);
|
|
119
|
-
} catch {
|
|
120
|
-
db.run(
|
|
121
|
-
`UPDATE stamps SET status = 'failed', last_error = ? WHERE id = ?`,
|
|
122
|
-
["proof file missing on disk", row.id]
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export {
|
|
129
|
-
getDataDir,
|
|
130
|
-
loadConfig,
|
|
131
|
-
getDb,
|
|
132
|
-
backupDb
|
|
133
|
-
};
|
package/dist/watch-MF5SD2H6.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getDb,
|
|
3
|
-
loadConfig
|
|
4
|
-
} from "./chunk-Y3F7WBP6.js";
|
|
5
|
-
|
|
6
|
-
// src/tools/watch.ts
|
|
7
|
-
async function watchPending(intervalMinutes = 5) {
|
|
8
|
-
const config = loadConfig();
|
|
9
|
-
const db = getDb();
|
|
10
|
-
process.stdout.write(`Watching pending stamps every ${intervalMinutes} min. Ctrl+C to stop.
|
|
11
|
-
|
|
12
|
-
`);
|
|
13
|
-
function tick() {
|
|
14
|
-
const rows = db.all(
|
|
15
|
-
`SELECT id, hash, status, attempt_count, bitcoin_block, confirmed_at FROM stamps WHERE status != 'confirmed' ORDER BY created_at DESC`
|
|
16
|
-
);
|
|
17
|
-
const confirmed = db.get(`SELECT COUNT(*) as n FROM stamps WHERE status = 'confirmed'`);
|
|
18
|
-
process.stdout.write(`${now()} \u2014 ${rows.length} pendientes, ${confirmed.n} confirmados
|
|
19
|
-
`);
|
|
20
|
-
for (const row of rows) {
|
|
21
|
-
process.stdout.write(` ${row.id.slice(0, 8)} ${row.status} (${row.attempt_count} intentos)
|
|
22
|
-
`);
|
|
23
|
-
}
|
|
24
|
-
if (rows.length === 0) {
|
|
25
|
-
process.stdout.write(` (ning\xFAn sello pendiente)
|
|
26
|
-
`);
|
|
27
|
-
}
|
|
28
|
-
process.stdout.write("\n");
|
|
29
|
-
}
|
|
30
|
-
function now() {
|
|
31
|
-
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
32
|
-
}
|
|
33
|
-
function loop() {
|
|
34
|
-
tick();
|
|
35
|
-
setTimeout(loop, Math.max(6e4, intervalMinutes * 60 * 1e3));
|
|
36
|
-
}
|
|
37
|
-
loop();
|
|
38
|
-
}
|
|
39
|
-
export {
|
|
40
|
-
watchPending
|
|
41
|
-
};
|