@helm-protocol/ttt-mcp 0.3.1 → 0.3.3

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 CHANGED
@@ -2,7 +2,19 @@
2
2
 
3
3
  > Reference implementation of [draft-helmprotocol-tttps](https://datatracker.ietf.org/doc/draft-helmprotocol-tttps/) (IETF Experimental)
4
4
 
5
- **MCP Server for OpenTTT Proof of Time tools for AI agents**
5
+ **Proof-of-Time attestation Ed25519-signed timestamps with multi-source corroboration and explicit error bounds. IETF draft-helmprotocol-tttps**
6
+
7
+ ---
8
+
9
+ ## EU AI Act Art. 50 — AI-Generated Content Transparency
10
+
11
+ TTTPS provides cryptographic time-provenance for AI-generated content at the moment of creation. A `pot_generate` call anchors a tamper-evident timestamp to a cryptographic hash of the content record — independently of any embedded metadata.
12
+
13
+ **C2PA complementarity**: C2PA metadata is stripped during recapture, transcoding, and format conversion. TTTPS survives as an external anchor independently verifiable without metadata chain continuity — allowing forensic reconstruction of content provenance even when embedded markers are absent.
14
+
15
+ **GDPR-compatible by design**: PoT records contain no content and no personal identifiers. Each record binds a cryptographic hash (SHA-256 + HMAC-SHA256) to a multi-source time attestation. No plaintext content transits or is stored on Helm servers.
16
+
17
+ IETF specification: [`draft-helmprotocol-tttps`](https://datatracker.ietf.org/doc/draft-helmprotocol-tttps/) (ISE track). Contact: peter@kenosian.com.
6
18
 
7
19
  ---
8
20
 
@@ -43,12 +55,12 @@ Claude workflow → [context compressed] → agents call pot_query(eventId)
43
55
  ### Claude Code
44
56
 
45
57
  ```bash
46
- claude mcp add ttt -- npx -y @helm-protocol/ttt-mcp@0.3.0
58
+ claude mcp add ttt -- npx -y @helm-protocol/ttt-mcp@0.3.2
47
59
  ```
48
60
 
49
61
  With an API key (raises the free limit to your plan's monthly quota):
50
62
  ```bash
51
- claude mcp add ttt -e TTT_API_KEY=your-key -- npx -y @helm-protocol/ttt-mcp@0.3.0
63
+ claude mcp add ttt -e TTT_API_KEY=your-key -- npx -y @helm-protocol/ttt-mcp@0.3.2
52
64
  ```
53
65
 
54
66
  ### Claude Desktop
@@ -60,7 +72,7 @@ Add to `claude_desktop_config.json`:
60
72
  "mcpServers": {
61
73
  "ttt": {
62
74
  "command": "npx",
63
- "args": ["-y", "@helm-protocol/ttt-mcp@0.3.0"],
75
+ "args": ["-y", "@helm-protocol/ttt-mcp@0.3.2"],
64
76
  "env": { "TTT_API_KEY": "your-key" }
65
77
  }
66
78
  }
@@ -69,7 +81,7 @@ Add to `claude_desktop_config.json`:
69
81
 
70
82
  ### Cursor
71
83
 
72
- [![Add to Cursor](https://img.shields.io/badge/Add%20to%20Cursor-1a1a1a?style=flat&logo=cursor&logoColor=white)](https://cursor.com/install-mcp?name=ttt&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBoZWxtLXByb3RvY29sL3R0dC1tY3BAMC4zLjAiXX0=)
84
+ [![Add to Cursor](https://img.shields.io/badge/Add%20to%20Cursor-1a1a1a?style=flat&logo=cursor&logoColor=white)](https://cursor.com/install-mcp?name=ttt&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBoZWxtLXByb3RvY29sL3R0dC1tY3BAMC4zLjIiXX0=)
73
85
 
74
86
  One-click install, or add the same `mcpServers` block above to `.cursor/mcp.json`.
75
87
 
@@ -115,12 +127,13 @@ pot_graph(eventId: "step-2", depth: 5)
115
127
 
116
128
  ---
117
129
 
118
- ## 7 Tools
130
+ ## 8 Tools
119
131
 
120
132
  | Tool | Purpose |
121
133
  |------|---------|
122
134
  | `pot_generate` | Stamp a workflow step with a cryptographic timestamp |
123
135
  | `pot_verify` | Verify a PoT signature |
136
+ | `pot_verify_v08` | Verify a draft-08 §3 Payload Digest record |
124
137
  | `pot_query` | O(1) exact lookup by eventId — core amnesia recovery |
125
138
  | `pot_graph` | Traverse causal DAG (backward + forward chain) |
126
139
  | `pot_checkpoint` | Roll up events into a compressed summary — use every ~100 events or before long tasks |
@@ -133,7 +146,7 @@ pot_graph(eventId: "step-2", depth: 5)
133
146
 
134
147
  ### pot_generate
135
148
 
136
- Stamp a workflow step with a cryptographic timestamp. For Claude Code: use `eventId` + `prevEventId`. For DeFi: use `txHash` + `chainId` + `poolAddress`. One of `eventId` or `txHash` is required.
149
+ Stamp a workflow step with a cryptographic timestamp. For Claude Code: use `eventId` + `prevEventId`. For DeFi: use `txHash` + `chainId` + `poolAddress`. To bind the attestation to a specific piece of content (draft-08 §3 Payload Digest), also supply `contentDigest`. One of `eventId`, `txHash`, or `contentDigest` is required.
137
150
 
138
151
  | Parameter | Type | Required | Description |
139
152
  |-----------|------|----------|-------------|
@@ -142,6 +155,19 @@ Stamp a workflow step with a cryptographic timestamp. For Claude Code: use `even
142
155
  | txHash | string | Either/or | Transaction hash (DeFi, hex with 0x prefix) |
143
156
  | chainId | number | No | EVM chain ID (DeFi) |
144
157
  | poolAddress | string | No | DEX pool contract address (DeFi) |
158
+ | contentDigest | string | Either/or | SHA-256 digest (lowercase hex, 64 chars) of the content this record attests to. Computed by the caller — the server never sees the content. When the local time synthesis meets draft-08's own requirements (≥3 independent sources, a representable error bound), the response includes a spec-conformant `potRecordV08` binary record (hex); otherwise `potRecordV08Error` explains why not. |
159
+ | ctxId | string | No | draft-08 §3.3 context identifier (Commitment domain separator, max 255 octets). Defaults to a fixed server value; MAY be public. |
160
+
161
+ ### pot_verify_v08
162
+
163
+ Verify a draft-08 §3 record produced by `pot_generate`'s `potRecordV08` field: recomputes the Commitment, checks the Ed25519 signature, and — if `content` is supplied — checks it against the record's Payload Digest.
164
+
165
+ | Parameter | Type | Required | Description |
166
+ |-----------|------|----------|-------------|
167
+ | potRecordV08 | string | Yes | Hex-encoded 184 or 216-octet record |
168
+ | ctxId | string | No | Must match what `pot_generate` used, or verification fails |
169
+ | issuerPubKey | string | No | Hex-encoded 32-byte raw Ed25519 public key. Defaults to this server's own key. |
170
+ | content | string | No | Payload to check against the record's Payload Digest field |
145
171
 
146
172
  ### pot_query
147
173
 
package/dist/index.js CHANGED
@@ -101,16 +101,20 @@ function toolSuccess(result) {
101
101
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
102
102
  }
103
103
  function buildMcpServer() {
104
- const s = new import_mcp.McpServer({ name: "ttt-mcp", version: "0.3.1" });
104
+ const s = new import_mcp.McpServer({ name: "ttt-mcp", version: "0.3.2" });
105
105
  s.tool(
106
106
  "pot_generate",
107
- "Generate a cryptographic Proof of Time timestamp (draft-helmprotocol-tttps, https://datatracker.ietf.org/doc/draft-helmprotocol-tttps/). For Claude Code workflows: use eventId + prevEventId to build a causal chain. For DeFi: use txHash + chainId + poolAddress. Either eventId or txHash is required.",
107
+ "Generate a cryptographic Proof of Time timestamp (draft-helmprotocol-tttps, https://datatracker.ietf.org/doc/draft-helmprotocol-tttps/). For Claude Code workflows: use eventId + prevEventId to build a causal chain. For DeFi: use txHash + chainId + poolAddress. For a spec-conformant draft-08 \xA73 record binding this attestation to a specific piece of content, also supply contentDigest. One of eventId, txHash, or contentDigest is required.",
108
108
  {
109
109
  eventId: import_zod.z.string().optional().describe("Workflow step identifier (Claude Code). E.g. 'refactor_auth_step1'"),
110
110
  prevEventId: import_zod.z.string().optional().describe("Previous step's eventId \u2014 links steps into a causal chain"),
111
111
  txHash: import_zod.z.string().optional().describe("Transaction hash (DeFi, hex with 0x prefix)"),
112
112
  chainId: import_zod.z.number().optional().describe("EVM chain ID (DeFi, e.g. 8453 for Base)"),
113
- poolAddress: import_zod.z.string().optional().describe("DEX pool contract address (DeFi)")
113
+ poolAddress: import_zod.z.string().optional().describe("DEX pool contract address (DeFi)"),
114
+ contentDigest: import_zod.z.string().regex(/^[0-9a-f]{64}$/).optional().describe(
115
+ "SHA-256 digest (lowercase hex, 64 characters) of the content this record attests to. Computed by the caller \u2014 the server never sees the content itself. When supplied, and the local time synthesis meets draft-08's own requirements (>=3 independent sources, a representable error bound), the response includes a spec-conformant potRecordV08 binary record (hex-encoded) in addition to the usual potHash fields; otherwise potRecordV08Error explains why it could not be produced."
116
+ ),
117
+ ctxId: import_zod.z.string().max(255).optional().describe("draft-08 \xA73.3 context identifier (domain separator for the Commitment). Defaults to a fixed server value if omitted; MAY be public.")
114
118
  },
115
119
  { title: "Generate Proof of Time", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
116
120
  async (args) => {
@@ -141,6 +145,25 @@ function buildMcpServer() {
141
145
  }
142
146
  }
143
147
  );
148
+ s.tool(
149
+ "pot_verify_v08",
150
+ "Verify a draft-helmprotocol-tttps-08 \xA73 Proof-of-Time record (as produced by pot_generate's potRecordV08 field): recomputes the Commitment and checks the Ed25519 signature, and \u2014 if content is supplied \u2014 recomputes SHA-256(content) and checks it against the record's Payload Digest field.",
151
+ {
152
+ potRecordV08: import_zod.z.string().describe("Hex-encoded 184 or 216-octet record from pot_generate's potRecordV08 field"),
153
+ ctxId: import_zod.z.string().max(255).optional().describe("Context identifier the record was generated under. Must match what pot_generate used, or verification fails."),
154
+ issuerPubKey: import_zod.z.string().optional().describe("Hex-encoded 32-byte raw Ed25519 issuer public key. Defaults to this server's own key."),
155
+ content: import_zod.z.string().optional().describe("The payload (utf8) to check against the record's Payload Digest field, if available")
156
+ },
157
+ { title: "Verify draft-08 Proof-of-Time Record", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
158
+ async (args) => {
159
+ try {
160
+ const result = await (0, import_tools.potVerifyV08)(args);
161
+ return toolSuccess(result);
162
+ } catch (err) {
163
+ return toolError(err);
164
+ }
165
+ }
166
+ );
144
167
  s.tool(
145
168
  "pot_query",
146
169
  "Query Proof of Time records. Use eventId for exact O(1) lookup of a specific workflow step (collision probability 2^-256). Use startTime/endTime for time-range queries.",
@@ -234,7 +257,7 @@ async function main() {
234
257
  const httpServer = (0, import_http.createServer)(async (req, res) => {
235
258
  if (req.method === "GET" && (req.url === "/health" || req.url === "/ping")) {
236
259
  res.writeHead(200, { "Content-Type": "application/json" });
237
- res.end(JSON.stringify({ status: "ok", server: "ttt-mcp", version: "0.3.1" }));
260
+ res.end(JSON.stringify({ status: "ok", server: "ttt-mcp", version: "0.3.2" }));
238
261
  return;
239
262
  }
240
263
  if (req.method === "POST") {
@@ -0,0 +1,212 @@
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
+ var pot_record_v08_exports = {};
20
+ __export(pot_record_v08_exports, {
21
+ MTI_INTEGRITY_ALG_SHA256: () => MTI_INTEGRITY_ALG_SHA256,
22
+ RECORD_SIZE_HOLDER: () => RECORD_SIZE_HOLDER,
23
+ RECORD_SIZE_NO_HOLDER: () => RECORD_SIZE_NO_HOLDER,
24
+ RESERVED_ERROR_BOUND: () => RESERVED_ERROR_BOUND,
25
+ assemblePotRecordV08: () => assemblePotRecordV08,
26
+ computeCommitmentSha256: () => computeCommitmentSha256,
27
+ decodePotRecordV08: () => decodePotRecordV08,
28
+ ed25519KeyFromSeed: () => ed25519KeyFromSeed,
29
+ ed25519PublicKeyFromRaw: () => ed25519PublicKeyFromRaw,
30
+ generatePotRecordV08: () => generatePotRecordV08,
31
+ rawPublicKeyOf: () => rawPublicKeyOf,
32
+ verifyPotRecordV08: () => verifyPotRecordV08
33
+ });
34
+ module.exports = __toCommonJS(pot_record_v08_exports);
35
+ var import_crypto = require("crypto");
36
+ const ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
37
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
38
+ function ed25519KeyFromSeed(seed) {
39
+ if (seed.length !== 32) throw new Error("Ed25519 seed must be 32 bytes");
40
+ const der = Buffer.concat([ED25519_PKCS8_PREFIX, seed]);
41
+ return (0, import_crypto.createPrivateKey)({ key: der, format: "der", type: "pkcs8" });
42
+ }
43
+ function ed25519PublicKeyFromRaw(pub) {
44
+ if (pub.length !== 32) throw new Error("Ed25519 public key must be 32 bytes");
45
+ const der = Buffer.concat([ED25519_SPKI_PREFIX, pub]);
46
+ return (0, import_crypto.createPublicKey)({ key: der, format: "der", type: "spki" });
47
+ }
48
+ function rawPublicKeyOf(key) {
49
+ return key.export({ type: "spki", format: "der" }).subarray(-32);
50
+ }
51
+ const PRE_OFFSET = {
52
+ HEADER: 0,
53
+ // Version(4)|Tier(4), Flags(8), Integrity Alg(16) — 4 bytes
54
+ SRC_ERR: 4,
55
+ // Src Cnt(8), Error Bound(24) — 4 bytes
56
+ TIMESTAMP: 8,
57
+ // 8 bytes
58
+ ISSUER_KEY_ID: 16,
59
+ // 8 bytes
60
+ NONCE: 24,
61
+ // 32 bytes
62
+ PAYLOAD_DIGEST: 56,
63
+ // 32 bytes
64
+ HOLDER_KEY: 88
65
+ // 32 bytes, iff Flags bit 0 = 1
66
+ };
67
+ const PRE_LEN_NO_HOLDER = 88;
68
+ const PRE_LEN_HOLDER = 120;
69
+ const COMMITMENT_LEN = 32;
70
+ const SIGNATURE_LEN = 64;
71
+ const RECORD_SIZE_NO_HOLDER = PRE_LEN_NO_HOLDER + COMMITMENT_LEN + SIGNATURE_LEN;
72
+ const RECORD_SIZE_HOLDER = PRE_LEN_HOLDER + COMMITMENT_LEN + SIGNATURE_LEN;
73
+ const MTI_INTEGRITY_ALG_SHA256 = 1;
74
+ const RESERVED_ERROR_BOUND = 16777215;
75
+ function encodeFieldsPre(f) {
76
+ if (f.version < 0 || f.version > 15) throw new Error("version must fit in 4 bits");
77
+ if (f.tier < 0 || f.tier > 15) throw new Error("tier must fit in 4 bits");
78
+ if (f.integrityAlg < 0 || f.integrityAlg > 65535) throw new Error("integrityAlg must fit in 16 bits");
79
+ if (f.srcCnt < 3) throw new Error("Src Cnt MUST be at least 3 (draft-08 Section 3.2)");
80
+ if (f.srcCnt > 255) throw new Error("srcCnt must fit in 8 bits");
81
+ if (f.errorBoundUs === RESERVED_ERROR_BOUND) {
82
+ throw new Error("Error Bound 0xFFFFFF is reserved and MUST NOT be emitted (draft-08 Section 3.2)");
83
+ }
84
+ if (f.errorBoundUs < 0 || f.errorBoundUs > 16777215) throw new Error("errorBoundUs must fit in 24 bits");
85
+ if (f.issuerKeyId.length !== 8) throw new Error("issuerKeyId must be 8 bytes");
86
+ if (f.nonce.length !== 32) throw new Error("nonce must be 32 bytes");
87
+ if (f.payloadDigest.length !== 32) throw new Error("payloadDigest must be 32 bytes (SHA-256)");
88
+ if (f.holderKey !== void 0 && f.holderKey.length !== 32) {
89
+ throw new Error("holderKey must be 32 bytes");
90
+ }
91
+ const hasHolder = f.holderKey !== void 0;
92
+ const buf = Buffer.alloc(hasHolder ? PRE_LEN_HOLDER : PRE_LEN_NO_HOLDER);
93
+ buf.writeUInt8(f.version << 4 | f.tier, PRE_OFFSET.HEADER);
94
+ buf.writeUInt8(hasHolder ? 128 : 0, PRE_OFFSET.HEADER + 1);
95
+ buf.writeUInt16BE(f.integrityAlg, PRE_OFFSET.HEADER + 2);
96
+ buf.writeUInt8(f.srcCnt, PRE_OFFSET.SRC_ERR);
97
+ buf.writeUIntBE(f.errorBoundUs, PRE_OFFSET.SRC_ERR + 1, 3);
98
+ buf.writeBigUInt64BE(f.timestampNs, PRE_OFFSET.TIMESTAMP);
99
+ f.issuerKeyId.copy(buf, PRE_OFFSET.ISSUER_KEY_ID);
100
+ f.nonce.copy(buf, PRE_OFFSET.NONCE);
101
+ f.payloadDigest.copy(buf, PRE_OFFSET.PAYLOAD_DIGEST);
102
+ if (hasHolder) f.holderKey.copy(buf, PRE_OFFSET.HOLDER_KEY);
103
+ return buf;
104
+ }
105
+ function computeCommitmentSha256(fieldsPre, ctxId) {
106
+ const ctxBytes = Buffer.from(ctxId, "utf8");
107
+ if (ctxBytes.length > 255) {
108
+ throw new Error("ctx_id exceeds 255 octets \u2014 not usable with integrity algorithm 0x0001");
109
+ }
110
+ return (0, import_crypto.createHash)("sha256").update(Buffer.concat([Buffer.from([ctxBytes.length]), ctxBytes, Buffer.from([0]), fieldsPre])).digest();
111
+ }
112
+ function assemblePotRecordV08(fields, ctxId, issuerPrivateKey) {
113
+ const fieldsPre = encodeFieldsPre(fields);
114
+ const commitment = computeCommitmentSha256(fieldsPre, ctxId);
115
+ const p = Buffer.concat([fieldsPre, commitment]);
116
+ const signature = (0, import_crypto.sign)(null, p, issuerPrivateKey);
117
+ return Buffer.concat([p, signature]);
118
+ }
119
+ function generatePotRecordV08(fields, ctxId, issuerPrivateKeySeed) {
120
+ return assemblePotRecordV08(fields, ctxId, ed25519KeyFromSeed(issuerPrivateKeySeed));
121
+ }
122
+ function decodePotRecordV08(record) {
123
+ if (record.length < 2) throw new Error("record too short to read Flags");
124
+ const flags = record.readUInt8(PRE_OFFSET.HEADER + 1);
125
+ const hasHolder = (flags & 128) !== 0;
126
+ const expectedLen = hasHolder ? RECORD_SIZE_HOLDER : RECORD_SIZE_NO_HOLDER;
127
+ if (record.length !== expectedLen) {
128
+ throw new Error(`expected ${expectedLen} octets for Flags bit0=${hasHolder ? 1 : 0}, got ${record.length}`);
129
+ }
130
+ const versionTier = record.readUInt8(PRE_OFFSET.HEADER);
131
+ const version = versionTier >> 4 & 15;
132
+ const tier = versionTier & 15;
133
+ const integrityAlg = record.readUInt16BE(PRE_OFFSET.HEADER + 2);
134
+ const srcCnt = record.readUInt8(PRE_OFFSET.SRC_ERR);
135
+ const errorBoundUs = record.readUIntBE(PRE_OFFSET.SRC_ERR + 1, 3);
136
+ const timestampNs = record.readBigUInt64BE(PRE_OFFSET.TIMESTAMP);
137
+ const issuerKeyId = Buffer.from(record.subarray(PRE_OFFSET.ISSUER_KEY_ID, PRE_OFFSET.ISSUER_KEY_ID + 8));
138
+ const nonce = Buffer.from(record.subarray(PRE_OFFSET.NONCE, PRE_OFFSET.NONCE + 32));
139
+ const payloadDigest = Buffer.from(record.subarray(PRE_OFFSET.PAYLOAD_DIGEST, PRE_OFFSET.PAYLOAD_DIGEST + 32));
140
+ const preLen = hasHolder ? PRE_LEN_HOLDER : PRE_LEN_NO_HOLDER;
141
+ const holderKey = hasHolder ? Buffer.from(record.subarray(PRE_OFFSET.HOLDER_KEY, PRE_OFFSET.HOLDER_KEY + 32)) : void 0;
142
+ const fieldsPre = Buffer.from(record.subarray(0, preLen));
143
+ const commitment = Buffer.from(record.subarray(preLen, preLen + COMMITMENT_LEN));
144
+ const signature = Buffer.from(record.subarray(preLen + COMMITMENT_LEN, preLen + COMMITMENT_LEN + SIGNATURE_LEN));
145
+ const p = Buffer.from(record.subarray(0, preLen + COMMITMENT_LEN));
146
+ return {
147
+ version,
148
+ tier,
149
+ flags,
150
+ integrityAlg,
151
+ srcCnt,
152
+ errorBoundUs,
153
+ timestampNs,
154
+ issuerKeyId,
155
+ nonce,
156
+ payloadDigest,
157
+ holderKey,
158
+ commitment,
159
+ signature,
160
+ fieldsPre,
161
+ p
162
+ };
163
+ }
164
+ function verifyPotRecordV08(record, ctxId, issuerPublicKeyRaw, content) {
165
+ let decoded;
166
+ try {
167
+ decoded = decodePotRecordV08(record);
168
+ } catch (e) {
169
+ return { verdict: "rejected", reason: e instanceof Error ? e.message : String(e) };
170
+ }
171
+ if (decoded.version !== 1) {
172
+ return { verdict: "rejected", reason: `unknown version ${decoded.version}, expected 1` };
173
+ }
174
+ if (decoded.integrityAlg !== MTI_INTEGRITY_ALG_SHA256) {
175
+ return { verdict: "rejected", reason: `unimplemented integrity algorithm 0x${decoded.integrityAlg.toString(16)}` };
176
+ }
177
+ if (decoded.errorBoundUs === RESERVED_ERROR_BOUND) {
178
+ return { verdict: "rejected", reason: "Error Bound carries the reserved value 0xFFFFFF" };
179
+ }
180
+ const expectedCommitment = computeCommitmentSha256(decoded.fieldsPre, ctxId);
181
+ if (!expectedCommitment.equals(decoded.commitment)) {
182
+ return { verdict: "rejected", reason: "commitment mismatch" };
183
+ }
184
+ const publicKey = ed25519PublicKeyFromRaw(issuerPublicKeyRaw);
185
+ if (!(0, import_crypto.verify)(null, decoded.p, publicKey, decoded.signature)) {
186
+ return { verdict: "rejected", reason: "Ed25519 signature invalid" };
187
+ }
188
+ let payloadDigestMatchesContent;
189
+ if (content !== void 0) {
190
+ const recomputed = (0, import_crypto.createHash)("sha256").update(content).digest();
191
+ payloadDigestMatchesContent = recomputed.equals(decoded.payloadDigest);
192
+ if (!payloadDigestMatchesContent) {
193
+ return { verdict: "rejected", reason: "Payload Digest does not match supplied content", payloadDigestMatchesContent };
194
+ }
195
+ }
196
+ return { verdict: "intact", payloadDigestMatchesContent };
197
+ }
198
+ // Annotate the CommonJS export names for ESM import in node:
199
+ 0 && (module.exports = {
200
+ MTI_INTEGRITY_ALG_SHA256,
201
+ RECORD_SIZE_HOLDER,
202
+ RECORD_SIZE_NO_HOLDER,
203
+ RESERVED_ERROR_BOUND,
204
+ assemblePotRecordV08,
205
+ computeCommitmentSha256,
206
+ decodePotRecordV08,
207
+ ed25519KeyFromSeed,
208
+ ed25519PublicKeyFromRaw,
209
+ generatePotRecordV08,
210
+ rawPublicKeyOf,
211
+ verifyPotRecordV08
212
+ });
package/dist/tools.js CHANGED
@@ -35,6 +35,7 @@ __export(tools_exports, {
35
35
  potQuery: () => potQuery,
36
36
  potStats: () => potStats,
37
37
  potVerify: () => potVerify,
38
+ potVerifyV08: () => potVerifyV08,
38
39
  redis: () => redis,
39
40
  restoreDagEntry: () => restoreDagEntry,
40
41
  tttsFreshnessSeal: () => tttsFreshnessSeal,
@@ -44,6 +45,8 @@ module.exports = __toCommonJS(tools_exports);
44
45
  var import_openttt = require("openttt");
45
46
  var import_telemetry = require("./telemetry");
46
47
  var import_server = require("./server");
48
+ var import_crypto = require("crypto");
49
+ var import_pot_record_v08 = require("./pot_record_v08");
47
50
  var import_ioredis = __toESM(require("ioredis"));
48
51
  const GrgPipeline = (
49
52
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -72,6 +75,16 @@ const timeSynth = new import_openttt.TimeSynthesis();
72
75
  const adaptiveSwitch = new import_openttt.AdaptiveSwitch();
73
76
  const potSigner = new import_openttt.PotSigner();
74
77
  const startedAt = Date.now();
78
+ const potSignerPrivateKeyV08 = (0, import_crypto.createPrivateKey)({
79
+ key: Buffer.from(potSigner.getPrivateKeyHex(), "hex"),
80
+ format: "der",
81
+ type: "pkcs8"
82
+ });
83
+ const potSignerPublicKeyRawV08 = Buffer.from(potSigner.getPubKeyHex(), "hex").subarray(-32);
84
+ const potSignerIssuerKeyIdV08 = (0, import_crypto.createHash)("sha256").update(potSignerPublicKeyRawV08).digest().subarray(0, 8);
85
+ const DEFAULT_CTX_ID_V08 = "openttt-mcp/pot_generate";
86
+ const DEFAULT_TIER_V08 = 3;
87
+ const CONTENT_DIGEST_HEX_RE = /^[0-9a-f]{64}$/;
75
88
  const POT_LOG_MAX = 1e4;
76
89
  const potLog = [];
77
90
  const potByEventId = /* @__PURE__ */ new Map();
@@ -172,12 +185,15 @@ function restoreDagEntry(entry) {
172
185
  }
173
186
  }
174
187
  async function potGenerate(args) {
175
- if (!args.eventId && !args.txHash) {
176
- throw new Error("Either eventId (Claude Code) or txHash (DeFi) is required");
188
+ if (!args.eventId && !args.txHash && !args.contentDigest) {
189
+ throw new Error("One of eventId (Claude Code), txHash (DeFi), or contentDigest is required");
190
+ }
191
+ if (args.contentDigest !== void 0 && !CONTENT_DIGEST_HEX_RE.test(args.contentDigest)) {
192
+ throw new Error("contentDigest must be a 64-character lowercase hex SHA-256 digest");
177
193
  }
178
194
  (0, import_telemetry.telemetryIncrement)("pot_generate");
179
195
  const apiKey = resolvePaidApiKey();
180
- if (apiKey && args.eventId) {
196
+ if (apiKey && args.eventId && !args.contentDigest) {
181
197
  const { data, advisory } = await (0, import_server.delegateToServer)({
182
198
  apiKey,
183
199
  method: "POST",
@@ -288,6 +304,33 @@ async function potGenerate(args) {
288
304
  });
289
305
  }
290
306
  updateLastPot(pot.timestamp, pot.stratum, pot.sources);
307
+ let potRecordV08;
308
+ let potRecordV08CtxId;
309
+ let potRecordV08Error;
310
+ if (args.contentDigest !== void 0) {
311
+ const ctxId = args.ctxId ?? DEFAULT_CTX_ID_V08;
312
+ const errorBoundUs = Math.round(pot.uncertainty * 1e3);
313
+ if (isOfflineFallback || pot.sources < 3) {
314
+ potRecordV08Error = `Src Cnt is ${pot.sources}, but draft-08 \xA73.2 requires at least 3 independent time sources`;
315
+ } else if (errorBoundUs < 0 || errorBoundUs >= import_pot_record_v08.RESERVED_ERROR_BOUND) {
316
+ potRecordV08Error = `Error Bound ${errorBoundUs}us is not representable in the 24-bit field (or hits the reserved value)`;
317
+ } else {
318
+ const fields = {
319
+ version: 1,
320
+ tier: DEFAULT_TIER_V08,
321
+ integrityAlg: import_pot_record_v08.MTI_INTEGRITY_ALG_SHA256,
322
+ srcCnt: pot.sources,
323
+ errorBoundUs,
324
+ timestampNs: pot.timestamp,
325
+ issuerKeyId: potSignerIssuerKeyIdV08,
326
+ nonce: (0, import_crypto.randomBytes)(32),
327
+ payloadDigest: Buffer.from(args.contentDigest, "hex")
328
+ };
329
+ const record = (0, import_pot_record_v08.assemblePotRecordV08)(fields, ctxId, potSignerPrivateKeyV08);
330
+ potRecordV08 = record.toString("hex");
331
+ potRecordV08CtxId = ctxId;
332
+ }
333
+ }
291
334
  return serialize({
292
335
  potHash,
293
336
  eventId: args.eventId ?? null,
@@ -305,7 +348,9 @@ async function potGenerate(args) {
305
348
  issuerPubKey: signature.issuerPubKey,
306
349
  signature: signature.signature,
307
350
  issuedAt: signature.issuedAt.toString()
308
- }
351
+ },
352
+ ...potRecordV08 !== void 0 && { potRecordV08, potRecordV08CtxId, potRecordV08IssuerPubKey: potSignerPublicKeyRawV08.toString("hex") },
353
+ ...potRecordV08Error !== void 0 && { potRecordV08Error }
309
354
  });
310
355
  }
311
356
  async function potVerify(args) {
@@ -329,6 +374,21 @@ async function potVerify(args) {
329
374
  verifiedAt: Date.now()
330
375
  });
331
376
  }
377
+ async function potVerifyV08(args) {
378
+ (0, import_telemetry.telemetryIncrement)("pot_verify_v08");
379
+ const record = Buffer.from(args.potRecordV08, "hex");
380
+ const ctxId = args.ctxId ?? DEFAULT_CTX_ID_V08;
381
+ const issuerPubKey = args.issuerPubKey !== void 0 ? Buffer.from(args.issuerPubKey, "hex") : potSignerPublicKeyRawV08;
382
+ const content = args.content !== void 0 ? Buffer.from(args.content, "utf8") : void 0;
383
+ const result = (0, import_pot_record_v08.verifyPotRecordV08)(record, ctxId, issuerPubKey, content);
384
+ return serialize({
385
+ verdict: result.verdict,
386
+ reason: result.reason ?? null,
387
+ payloadDigestMatchesContent: result.payloadDigestMatchesContent ?? null,
388
+ ctxId,
389
+ verifiedAt: Date.now()
390
+ });
391
+ }
332
392
  async function potQuery(args) {
333
393
  (0, import_telemetry.telemetryIncrement)("pot_query");
334
394
  const apiKey = resolvePaidApiKey();
@@ -590,6 +650,7 @@ async function potCheckpoint(args) {
590
650
  potQuery,
591
651
  potStats,
592
652
  potVerify,
653
+ potVerifyV08,
593
654
  redis,
594
655
  restoreDagEntry,
595
656
  tttsFreshnessSeal,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@helm-protocol/ttt-mcp",
3
- "version": "0.3.1",
4
- "description": "MCP Server for OpenTTT Proof of Time tools for AI agents",
3
+ "version": "0.3.3",
4
+ "description": "Proof-of-Time attestation \u2014 Ed25519-signed timestamps with multi-source corroboration and explicit error bounds. IETF draft-helmprotocol-tttps",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "ttt-mcp": "./dist/index.js"
@@ -11,7 +11,7 @@
11
11
  "README.md"
12
12
  ],
13
13
  "scripts": {
14
- "build": "esbuild index.ts tools.ts telemetry.ts auth.ts server.ts --platform=node --target=node18 --format=cjs --outdir=dist",
14
+ "build": "esbuild index.ts tools.ts telemetry.ts auth.ts server.ts pot_record_v08.ts --platform=node --target=node18 --format=cjs --outdir=dist",
15
15
  "start": "node dist/index.js",
16
16
  "dev": "npx ts-node index.ts",
17
17
  "test": "jest --forceExit",
@@ -20,16 +20,21 @@
20
20
  "jest": {
21
21
  "preset": "ts-jest",
22
22
  "testEnvironment": "node",
23
- "testMatch": ["**/__tests__/**/*.test.ts"],
23
+ "testMatch": [
24
+ "**/__tests__/**/*.test.ts"
25
+ ],
24
26
  "moduleNameMapper": {
25
27
  "^openttt$": "<rootDir>/node_modules/openttt/dist/index.js"
26
28
  },
27
29
  "transform": {
28
- "^.+\\.tsx?$": ["ts-jest", {
29
- "tsconfig": {
30
- "module": "CommonJS"
30
+ "^.+\\.tsx?$": [
31
+ "ts-jest",
32
+ {
33
+ "tsconfig": {
34
+ "module": "CommonJS"
35
+ }
31
36
  }
32
- }]
37
+ ]
33
38
  }
34
39
  },
35
40
  "keywords": [