@helm-protocol/ttt-mcp 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,12 +55,12 @@ Claude workflow → [context compressed] → agents call pot_query(eventId)
55
55
  ### Claude Code
56
56
 
57
57
  ```bash
58
- claude mcp add ttt -- npx -y @helm-protocol/ttt-mcp@0.3.2
58
+ claude mcp add ttt -- npx -y @helm-protocol/ttt-mcp@0.3.3
59
59
  ```
60
60
 
61
61
  With an API key (raises the free limit to your plan's monthly quota):
62
62
  ```bash
63
- claude mcp add ttt -e TTT_API_KEY=your-key -- npx -y @helm-protocol/ttt-mcp@0.3.2
63
+ claude mcp add ttt -e TTT_API_KEY=your-key -- npx -y @helm-protocol/ttt-mcp@0.3.3
64
64
  ```
65
65
 
66
66
  ### Claude Desktop
@@ -72,7 +72,7 @@ Add to `claude_desktop_config.json`:
72
72
  "mcpServers": {
73
73
  "ttt": {
74
74
  "command": "npx",
75
- "args": ["-y", "@helm-protocol/ttt-mcp@0.3.2"],
75
+ "args": ["-y", "@helm-protocol/ttt-mcp@0.3.3"],
76
76
  "env": { "TTT_API_KEY": "your-key" }
77
77
  }
78
78
  }
@@ -81,7 +81,7 @@ Add to `claude_desktop_config.json`:
81
81
 
82
82
  ### Cursor
83
83
 
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=)
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=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBoZWxtLXByb3RvY29sL3R0dC1tY3BAMC4zLjMiXX0=)
85
85
 
86
86
  One-click install, or add the same `mcpServers` block above to `.cursor/mcp.json`.
87
87
 
@@ -168,6 +168,12 @@ Verify a draft-08 §3 record produced by `pot_generate`'s `potRecordV08` field:
168
168
  | ctxId | string | No | Must match what `pot_generate` used, or verification fails |
169
169
  | issuerPubKey | string | No | Hex-encoded 32-byte raw Ed25519 public key. Defaults to this server's own key. |
170
170
  | content | string | No | Payload to check against the record's Payload Digest field |
171
+ | clientId | string | Required when `TTTPS_REQUIRE_REPLAY_LEDGER=1` | Stable caller identity for the replay key |
172
+ | sessionId | string | Required when `TTTPS_REQUIRE_REPLAY_LEDGER=1` | Active transport/session identifier for the replay key |
173
+
174
+ When `TTTPS_REQUIRE_V08_FRESHNESS=1`, `TTTPS_V08_MAX_SKEW_NS` MUST be set to a non-negative integer. The verifier rejects records outside `maxSkewNs + errorBoundUs`. When `TTTPS_REQUIRE_REPLAY_LEDGER=1`, a successful verification atomically claims `clientId/sessionId/nonce` in Redis with a 90-day TTL; Redis failure or a second claim is rejected. These flags are fail-closed controls.
175
+
176
+ The current MCP canonical wire profile is draft-11 PoT Record v2 (180 octets). `pot_generate_v2` and `pot_verify_v2` implement the fixed core record, including SHA-256 integrity over octets 0-79 and Ed25519 issuer authentication over octets 0-115. Draft-08 remains available through `pot_verify_v08` for legacy interoperability only. TLS binding proof is a separate 64/32-octet value; `TTTPS_V2_REQUIRE_BINDING=1` fails closed unless the server is running over direct TLS 1.3; stdio and plain HTTP have no TLS exporter. Configure `MCP_TLS_CERT_FILE` and `MCP_TLS_KEY_FILE` for the HTTPS mode. Replay claims use `(ctx_id, nonce)` with `TTTPS_REPLAY_TTL_SECONDS` (default 86400).
171
177
 
172
178
  ### pot_query
173
179
 
package/dist/index.js CHANGED
@@ -4,6 +4,10 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
4
4
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
5
5
  var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
6
6
  var import_http = require("http");
7
+ var import_https = require("https");
8
+ var import_fs = require("fs");
9
+ var import_tls_exporter = require("./tls_exporter");
10
+ var import_transport_context = require("./transport_context");
7
11
  var import_zod = require("zod");
8
12
  var import_tools = require("./tools");
9
13
  var import_auth = require("./auth");
@@ -102,7 +106,8 @@ function toolSuccess(result) {
102
106
  }
103
107
  function buildMcpServer() {
104
108
  const s = new import_mcp.McpServer({ name: "ttt-mcp", version: "0.3.2" });
105
- s.tool(
109
+ const registerTool = s.tool.bind(s);
110
+ registerTool(
106
111
  "pot_generate",
107
112
  "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
113
  {
@@ -126,7 +131,7 @@ function buildMcpServer() {
126
131
  }
127
132
  }
128
133
  );
129
- s.tool(
134
+ registerTool(
130
135
  "pot_verify",
131
136
  "Verify a Proof of Time using its hash and integrity shards. Returns validity, mode (turbo/full), and timestamp.",
132
137
  {
@@ -145,16 +150,58 @@ function buildMcpServer() {
145
150
  }
146
151
  }
147
152
  );
148
- s.tool(
153
+ registerTool(
154
+ "pot_generate_v2",
155
+ "Generate the draft-11 180-octet Proof-of-Time Record v2 core. TLS binding proof is computed only after a live TLS session exists.",
156
+ {
157
+ tsTaiUs: import_zod.z.string().describe("TAI timestamp in microseconds as a decimal string"),
158
+ dispersionUs: import_zod.z.number().int().nonnegative().describe("Uncertainty bound in microseconds"),
159
+ ctxId: import_zod.z.string().length(32).describe("16-octet context identifier encoded as hex"),
160
+ holderAuthData: import_zod.z.string().length(64).describe("32-octet holder public key or PSK digest encoded as hex"),
161
+ holderAuthType: import_zod.z.number().int().optional().describe("0x01 Ed25519 holder key (default) or 0x02 shared secret")
162
+ },
163
+ { title: "Generate draft-11 180-octet PoT v2", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
164
+ async (args) => {
165
+ try {
166
+ return toolSuccess(await (0, import_tools.potGenerateV2)(args));
167
+ } catch (err) {
168
+ return toolError(err);
169
+ }
170
+ }
171
+ );
172
+ registerTool(
173
+ "pot_verify_v2",
174
+ "Verify the draft-11 180-octet PoT Record v2 core. Binding-required mode fails closed in stdio because no TLS exporter session exists.",
175
+ {
176
+ potRecordV2: import_zod.z.string().length(360).describe("Hex-encoded 180-octet draft-11 v2 record"),
177
+ issuerPubKey: import_zod.z.string().length(64).optional().describe("Raw 32-octet issuer Ed25519 public key in hex"),
178
+ nowTaiUs: import_zod.z.string().optional().describe("Current TAI timestamp in microseconds"),
179
+ maxSkewUs: import_zod.z.string().optional().describe("Configured freshness allowance in microseconds"),
180
+ clientId: import_zod.z.string().optional(),
181
+ sessionId: import_zod.z.string().optional(),
182
+ bindingProof: import_zod.z.string().regex(/^(?:[0-9a-fA-F]{128}|[0-9a-fA-F]{64})$/).optional().describe("64-octet Ed25519 or 32-octet HMAC TLS binding proof")
183
+ },
184
+ { title: "Verify draft-11 180-octet PoT v2", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
185
+ async (args) => {
186
+ try {
187
+ return toolSuccess(await (0, import_tools.potVerifyV2)(args));
188
+ } catch (err) {
189
+ return toolError(err);
190
+ }
191
+ }
192
+ );
193
+ registerTool(
149
194
  "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.",
195
+ "Verify and, when configured, admit a draft-helmprotocol-tttps-08 \xA73 Proof-of-Time record: recomputes the Commitment and Ed25519 signature, applies configured freshness, and atomically claims client/session/nonce in Redis before admission.",
151
196
  {
152
197
  potRecordV08: import_zod.z.string().describe("Hex-encoded 184 or 216-octet record from pot_generate's potRecordV08 field"),
153
198
  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
199
  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")
200
+ content: import_zod.z.string().optional().describe("The payload (utf8) to check against the record's Payload Digest field, if available"),
201
+ clientId: import_zod.z.string().optional().describe("Stable caller identity for the durable replay ledger when TTTPS_REQUIRE_REPLAY_LEDGER=1"),
202
+ sessionId: import_zod.z.string().optional().describe("Active transport/session identifier for the durable replay ledger when TTTPS_REQUIRE_REPLAY_LEDGER=1")
156
203
  },
157
- { title: "Verify draft-08 Proof-of-Time Record", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
204
+ { title: "Verify draft-08 Proof-of-Time Record", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
158
205
  async (args) => {
159
206
  try {
160
207
  const result = await (0, import_tools.potVerifyV08)(args);
@@ -164,7 +211,7 @@ function buildMcpServer() {
164
211
  }
165
212
  }
166
213
  );
167
- s.tool(
214
+ registerTool(
168
215
  "pot_query",
169
216
  "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.",
170
217
  {
@@ -183,7 +230,7 @@ function buildMcpServer() {
183
230
  }
184
231
  }
185
232
  );
186
- s.tool(
233
+ registerTool(
187
234
  "pot_graph",
188
235
  "Traverse the causal chain of workflow steps. Given an eventId, returns the full backward chain (ancestors via prevEventId) and forward chain (steps that follow). Use after context compression to reconstruct the complete workflow timeline.",
189
236
  {
@@ -200,7 +247,7 @@ function buildMcpServer() {
200
247
  }
201
248
  }
202
249
  );
203
- s.tool(
250
+ registerTool(
204
251
  "pot_stats",
205
252
  "Get PoT statistics: total swaps, turbo/full counts, and turbo ratio for a given period.",
206
253
  { period: import_zod.z.enum(["day", "week", "month"]).describe("Time period for statistics") },
@@ -214,7 +261,7 @@ function buildMcpServer() {
214
261
  }
215
262
  }
216
263
  );
217
- s.tool(
264
+ registerTool(
218
265
  "pot_health",
219
266
  "Check PoT system health: time source status, subgraph sync, server uptime, and current mode.",
220
267
  {},
@@ -228,7 +275,7 @@ function buildMcpServer() {
228
275
  }
229
276
  }
230
277
  );
231
- s.tool(
278
+ registerTool(
232
279
  "pot_checkpoint",
233
280
  "Create a compressed rollup checkpoint of workflow history. Call this periodically to prevent token explosion when recovering from context compression. Returns checkpointId, compressed event history, chainIntact status, and nextCheckpointHint (recommended events before next checkpoint).",
234
281
  {
@@ -254,7 +301,7 @@ async function main() {
254
301
  await restoreDAGFromRedis();
255
302
  const port = process.env.PORT ? parseInt(process.env.PORT, 10) : null;
256
303
  if (port) {
257
- const httpServer = (0, import_http.createServer)(async (req, res) => {
304
+ const requestHandler = async (req, res) => {
258
305
  if (req.method === "GET" && (req.url === "/health" || req.url === "/ping")) {
259
306
  res.writeHead(200, { "Content-Type": "application/json" });
260
307
  res.end(JSON.stringify({ status: "ok", server: "ttt-mcp", version: "0.3.2" }));
@@ -309,9 +356,18 @@ async function main() {
309
356
  res.end(JSON.stringify({ error: "Internal server error" }));
310
357
  }
311
358
  }
312
- });
359
+ };
360
+ const certFile = process.env.MCP_TLS_CERT_FILE?.trim();
361
+ const keyFile = process.env.MCP_TLS_KEY_FILE?.trim();
362
+ if (certFile && !keyFile || !certFile && keyFile) {
363
+ throw new Error("MCP_TLS_CERT_FILE and MCP_TLS_KEY_FILE must be configured together");
364
+ }
365
+ const httpServer = certFile && keyFile ? (0, import_https.createServer)({ minVersion: "TLSv1.3", cert: (0, import_fs.readFileSync)(certFile), key: (0, import_fs.readFileSync)(keyFile) }, (req, res) => {
366
+ const exporter = (0, import_tls_exporter.extractTls13Exporter)(req);
367
+ return (0, import_transport_context.withTransportBinding)({ exporter, clientId: String(req.headers["x-ttt-client-id"] ?? ""), sessionId: String(req.headers["x-ttt-session-id"] ?? "") }, () => requestHandler(req, res));
368
+ }) : (0, import_http.createServer)((req, res) => (0, import_transport_context.withTransportBinding)({ clientId: String(req.headers["x-ttt-client-id"] ?? ""), sessionId: String(req.headers["x-ttt-session-id"] ?? "") }, () => requestHandler(req, res)));
313
369
  httpServer.listen(port, () => {
314
- console.error(`[ttt-mcp] OpenTTT MCP Server (HTTP) on port ${port}`);
370
+ console.error(`[ttt-mcp] OpenTTT MCP Server (${certFile ? "HTTPS/TLS1.3" : "HTTP"}) on port ${port}`);
315
371
  });
316
372
  } else {
317
373
  const stdioServer = buildMcpServer();
@@ -161,7 +161,7 @@ function decodePotRecordV08(record) {
161
161
  p
162
162
  };
163
163
  }
164
- function verifyPotRecordV08(record, ctxId, issuerPublicKeyRaw, content) {
164
+ function verifyPotRecordV08(record, ctxId, issuerPublicKeyRaw, content, freshness) {
165
165
  let decoded;
166
166
  try {
167
167
  decoded = decodePotRecordV08(record);
@@ -177,6 +177,15 @@ function verifyPotRecordV08(record, ctxId, issuerPublicKeyRaw, content) {
177
177
  if (decoded.errorBoundUs === RESERVED_ERROR_BOUND) {
178
178
  return { verdict: "rejected", reason: "Error Bound carries the reserved value 0xFFFFFF" };
179
179
  }
180
+ if (freshness !== void 0) {
181
+ if (freshness.maxSkewNs < 0n) {
182
+ return { verdict: "rejected", reason: "invalid freshness policy" };
183
+ }
184
+ const delta = decoded.timestampNs >= freshness.nowNs ? decoded.timestampNs - freshness.nowNs : freshness.nowNs - decoded.timestampNs;
185
+ if (delta > freshness.maxSkewNs + BigInt(decoded.errorBoundUs) * 1000n) {
186
+ return { verdict: "rejected", reason: "freshness window exceeded" };
187
+ }
188
+ }
180
189
  const expectedCommitment = computeCommitmentSha256(decoded.fieldsPre, ctxId);
181
190
  if (!expectedCommitment.equals(decoded.commitment)) {
182
191
  return { verdict: "rejected", reason: "commitment mismatch" };
@@ -0,0 +1,174 @@
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_v2_exports = {};
20
+ __export(pot_record_v2_exports, {
21
+ HOLDER_AUTH_ED25519: () => HOLDER_AUTH_ED25519,
22
+ HOLDER_AUTH_SHARED_SECRET: () => HOLDER_AUTH_SHARED_SECRET,
23
+ POT_V2_INTEGRITY_SHA256: () => POT_V2_INTEGRITY_SHA256,
24
+ POT_V2_SIZE: () => POT_V2_SIZE,
25
+ POT_V2_VERSION: () => POT_V2_VERSION,
26
+ computeV2BindingInput: () => computeV2BindingInput,
27
+ computeV2ExporterContext: () => computeV2ExporterContext,
28
+ decodePotRecordV2: () => decodePotRecordV2,
29
+ encodePotRecordV2: () => encodePotRecordV2,
30
+ verifyPotRecordV2: () => verifyPotRecordV2,
31
+ verifyV2BindingProof: () => verifyV2BindingProof
32
+ });
33
+ module.exports = __toCommonJS(pot_record_v2_exports);
34
+ var import_crypto = require("crypto");
35
+ const POT_V2_SIZE = 180;
36
+ const POT_V2_VERSION = 2;
37
+ const POT_V2_INTEGRITY_SHA256 = 1;
38
+ const HOLDER_AUTH_ED25519 = 1;
39
+ const HOLDER_AUTH_SHARED_SECRET = 2;
40
+ const OFF = {
41
+ VERSION: 0,
42
+ HOLDER_AUTH_TYPE: 1,
43
+ ALG_ID: 2,
44
+ TS: 4,
45
+ DISPERSION: 12,
46
+ CTX_ID: 16,
47
+ NONCE: 32,
48
+ HOLDER_AUTH_DATA: 48,
49
+ INTEGRITY_TAG: 80,
50
+ ISSUER_KEY_ID: 112,
51
+ ISSUER_SIG: 116
52
+ };
53
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
54
+ function requireLength(name, value, length) {
55
+ if (value.length !== length) throw new Error(`${name} must be ${length} bytes`);
56
+ }
57
+ function issuerPublicKeyFromRaw(raw) {
58
+ requireLength("issuer public key", raw, 32);
59
+ return (0, import_crypto.createPublicKey)({ key: Buffer.concat([ED25519_SPKI_PREFIX, raw]), format: "der", type: "spki" });
60
+ }
61
+ function integrityTag(signedPrefix) {
62
+ return (0, import_crypto.createHash)("sha256").update(signedPrefix).digest();
63
+ }
64
+ function encodePotRecordV2(fields, issuerPrivateKey) {
65
+ if (fields.holderAuthType !== HOLDER_AUTH_ED25519 && fields.holderAuthType !== HOLDER_AUTH_SHARED_SECRET) {
66
+ throw new Error("unsupported holder_auth_type");
67
+ }
68
+ if (fields.algId !== POT_V2_INTEGRITY_SHA256) throw new Error("unsupported alg_id");
69
+ if (fields.tsTaiUs < 0n || fields.tsTaiUs > 0xffffffffffffffffn) throw new Error("ts out of range");
70
+ if (fields.dispersionUs < 0 || fields.dispersionUs > 4294967295) throw new Error("dispersion out of range");
71
+ if (fields.issuerKeyId < 0 || fields.issuerKeyId > 4294967295) throw new Error("issuer_key_id out of range");
72
+ requireLength("ctx_id", fields.ctxId, 16);
73
+ requireLength("nonce", fields.nonce, 16);
74
+ requireLength("holder_auth_data", fields.holderAuthData, 32);
75
+ const prefix = Buffer.alloc(116);
76
+ prefix.writeUInt8(POT_V2_VERSION, OFF.VERSION);
77
+ prefix.writeUInt8(fields.holderAuthType, OFF.HOLDER_AUTH_TYPE);
78
+ prefix.writeUInt16BE(fields.algId, OFF.ALG_ID);
79
+ prefix.writeBigUInt64BE(fields.tsTaiUs, OFF.TS);
80
+ prefix.writeUInt32BE(fields.dispersionUs, OFF.DISPERSION);
81
+ fields.ctxId.copy(prefix, OFF.CTX_ID);
82
+ fields.nonce.copy(prefix, OFF.NONCE);
83
+ fields.holderAuthData.copy(prefix, OFF.HOLDER_AUTH_DATA);
84
+ integrityTag(prefix.subarray(0, 80)).copy(prefix, OFF.INTEGRITY_TAG);
85
+ prefix.writeUInt32BE(fields.issuerKeyId, OFF.ISSUER_KEY_ID);
86
+ const signature = (0, import_crypto.sign)(null, prefix, issuerPrivateKey);
87
+ return Buffer.concat([prefix, signature]);
88
+ }
89
+ function decodePotRecordV2(record) {
90
+ if (record.length !== POT_V2_SIZE) throw new Error(`expected 180 octets, got ${record.length}`);
91
+ const signedPrefix = record.subarray(0, 116);
92
+ return {
93
+ holderAuthType: record.readUInt8(OFF.HOLDER_AUTH_TYPE),
94
+ algId: record.readUInt16BE(OFF.ALG_ID),
95
+ tsTaiUs: record.readBigUInt64BE(OFF.TS),
96
+ dispersionUs: record.readUInt32BE(OFF.DISPERSION),
97
+ ctxId: Buffer.from(record.subarray(OFF.CTX_ID, OFF.CTX_ID + 16)),
98
+ nonce: Buffer.from(record.subarray(OFF.NONCE, OFF.NONCE + 16)),
99
+ holderAuthData: Buffer.from(record.subarray(OFF.HOLDER_AUTH_DATA, OFF.HOLDER_AUTH_DATA + 32)),
100
+ issuerKeyId: record.readUInt32BE(OFF.ISSUER_KEY_ID),
101
+ integrityTag: Buffer.from(record.subarray(OFF.INTEGRITY_TAG, OFF.INTEGRITY_TAG + 32)),
102
+ issuerSig: Buffer.from(record.subarray(OFF.ISSUER_SIG, POT_V2_SIZE)),
103
+ signedPrefix: Buffer.from(signedPrefix)
104
+ };
105
+ }
106
+ function computeV2ExporterContext(record) {
107
+ const decoded = decodePotRecordV2(record);
108
+ const bindingPrefix = Buffer.from("tttps-binding-v2\0", "ascii");
109
+ const recordHash = (0, import_crypto.createHash)("sha256").update(record).digest();
110
+ return (0, import_crypto.createHash)("sha256").update(Buffer.concat([bindingPrefix, recordHash, decoded.ctxId, decoded.nonce])).digest();
111
+ }
112
+ function computeV2BindingInput(record, exporterOutput) {
113
+ decodePotRecordV2(record);
114
+ if (exporterOutput.length !== 32) throw new Error("TLS exporter output must be 32 octets");
115
+ return Buffer.concat([Buffer.from("tttps-binding-v2\0", "ascii"), exporterOutput, record.subarray(16, 32), record.subarray(32, 48)]);
116
+ }
117
+ function verifyV2BindingProof(record, bindingProof, exporterOutput, sharedSecret) {
118
+ const decoded = decodePotRecordV2(record);
119
+ const bindingInput = computeV2BindingInput(record, exporterOutput);
120
+ if (decoded.holderAuthType === HOLDER_AUTH_ED25519) {
121
+ if (bindingProof.length !== 64) return { verdict: "rejected", reason: "Ed25519 binding proof must be 64 octets" };
122
+ const holderPublicKey = issuerPublicKeyFromRaw(decoded.holderAuthData);
123
+ return (0, import_crypto.verify)(null, bindingInput, holderPublicKey, bindingProof) ? { verdict: "intact" } : { verdict: "rejected", reason: "TLS exporter holder proof mismatch" };
124
+ }
125
+ if (decoded.holderAuthType === HOLDER_AUTH_SHARED_SECRET) {
126
+ if (!sharedSecret) return { verdict: "rejected", reason: "shared-secret holder binding is not configured" };
127
+ if (bindingProof.length !== 32) return { verdict: "rejected", reason: "HMAC binding proof must be 32 octets" };
128
+ const expectedDigest = (0, import_crypto.createHash)("sha256").update(sharedSecret).digest();
129
+ if (!(0, import_crypto.timingSafeEqual)(expectedDigest, decoded.holderAuthData)) return { verdict: "rejected", reason: "shared-secret digest mismatch" };
130
+ const expected = (0, import_crypto.createHmac)("sha256", sharedSecret).update(bindingInput).digest();
131
+ return (0, import_crypto.timingSafeEqual)(expected, bindingProof) ? { verdict: "intact" } : { verdict: "rejected", reason: "TLS exporter HMAC proof mismatch" };
132
+ }
133
+ return { verdict: "rejected", reason: "unsupported holder authentication type" };
134
+ }
135
+ function verifyPotRecordV2(record, issuerPublicKeyRaw, freshness) {
136
+ let decoded;
137
+ try {
138
+ decoded = decodePotRecordV2(record);
139
+ } catch (e) {
140
+ return { verdict: "rejected", reason: e instanceof Error ? e.message : String(e) };
141
+ }
142
+ if (record[OFF.VERSION] !== POT_V2_VERSION) return { verdict: "rejected", reason: "unknown version" };
143
+ if (decoded.holderAuthType !== HOLDER_AUTH_ED25519 && decoded.holderAuthType !== HOLDER_AUTH_SHARED_SECRET) {
144
+ return { verdict: "rejected", reason: "unsupported holder_auth_type" };
145
+ }
146
+ if (decoded.algId !== POT_V2_INTEGRITY_SHA256) return { verdict: "rejected", reason: "unsupported alg_id" };
147
+ if (!integrityTag(decoded.signedPrefix.subarray(0, 80)).equals(decoded.integrityTag)) return { verdict: "rejected", reason: "integrity mismatch" };
148
+ if (freshness) {
149
+ const delta = decoded.tsTaiUs >= freshness.nowTaiUs ? decoded.tsTaiUs - freshness.nowTaiUs : freshness.nowTaiUs - decoded.tsTaiUs;
150
+ if (delta > freshness.maxSkewUs + BigInt(decoded.dispersionUs)) return { verdict: "rejected", reason: "freshness window exceeded" };
151
+ }
152
+ try {
153
+ if (!(0, import_crypto.verify)(null, decoded.signedPrefix, issuerPublicKeyFromRaw(issuerPublicKeyRaw), decoded.issuerSig)) {
154
+ return { verdict: "rejected", reason: "issuer signature invalid" };
155
+ }
156
+ } catch (e) {
157
+ return { verdict: "rejected", reason: e instanceof Error ? e.message : String(e) };
158
+ }
159
+ return { verdict: "intact", nonce: decoded.nonce };
160
+ }
161
+ // Annotate the CommonJS export names for ESM import in node:
162
+ 0 && (module.exports = {
163
+ HOLDER_AUTH_ED25519,
164
+ HOLDER_AUTH_SHARED_SECRET,
165
+ POT_V2_INTEGRITY_SHA256,
166
+ POT_V2_SIZE,
167
+ POT_V2_VERSION,
168
+ computeV2BindingInput,
169
+ computeV2ExporterContext,
170
+ decodePotRecordV2,
171
+ encodePotRecordV2,
172
+ verifyPotRecordV2,
173
+ verifyV2BindingProof
174
+ });
@@ -0,0 +1,43 @@
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 tls_exporter_exports = {};
20
+ __export(tls_exporter_exports, {
21
+ TTTPS_V2_EXPORTER_LABEL: () => TTTPS_V2_EXPORTER_LABEL,
22
+ TTTPS_V2_EXPORTER_LENGTH: () => TTTPS_V2_EXPORTER_LENGTH,
23
+ exporterForTlsSocket: () => exporterForTlsSocket,
24
+ extractTls13Exporter: () => extractTls13Exporter
25
+ });
26
+ module.exports = __toCommonJS(tls_exporter_exports);
27
+ const TTTPS_V2_EXPORTER_LABEL = "EXPORTER-TTTPS-v2-Binding";
28
+ const TTTPS_V2_EXPORTER_LENGTH = 32;
29
+ function exporterForTlsSocket(socket) {
30
+ if (!socket.encrypted || typeof socket.exportKeyingMaterial !== "function") return void 0;
31
+ if (typeof socket.getProtocol === "function" && socket.getProtocol() !== "TLSv1.3") return void 0;
32
+ return (contextValue) => socket.exportKeyingMaterial(TTTPS_V2_EXPORTER_LENGTH, TTTPS_V2_EXPORTER_LABEL, contextValue);
33
+ }
34
+ function extractTls13Exporter(req) {
35
+ return exporterForTlsSocket(req.socket);
36
+ }
37
+ // Annotate the CommonJS export names for ESM import in node:
38
+ 0 && (module.exports = {
39
+ TTTPS_V2_EXPORTER_LABEL,
40
+ TTTPS_V2_EXPORTER_LENGTH,
41
+ exporterForTlsSocket,
42
+ extractTls13Exporter
43
+ });
package/dist/tools.js CHANGED
@@ -30,12 +30,14 @@ var tools_exports = {};
30
30
  __export(tools_exports, {
31
31
  potCheckpoint: () => potCheckpoint,
32
32
  potGenerate: () => potGenerate,
33
+ potGenerateV2: () => potGenerateV2,
33
34
  potGraph: () => potGraph,
34
35
  potHealth: () => potHealth,
35
36
  potQuery: () => potQuery,
36
37
  potStats: () => potStats,
37
38
  potVerify: () => potVerify,
38
39
  potVerifyV08: () => potVerifyV08,
40
+ potVerifyV2: () => potVerifyV2,
39
41
  redis: () => redis,
40
42
  restoreDagEntry: () => restoreDagEntry,
41
43
  tttsFreshnessSeal: () => tttsFreshnessSeal,
@@ -46,6 +48,8 @@ var import_openttt = require("openttt");
46
48
  var import_telemetry = require("./telemetry");
47
49
  var import_server = require("./server");
48
50
  var import_crypto = require("crypto");
51
+ var import_pot_record_v2 = require("./pot_record_v2");
52
+ var import_transport_context = require("./transport_context");
49
53
  var import_pot_record_v08 = require("./pot_record_v08");
50
54
  var import_ioredis = __toESM(require("ioredis"));
51
55
  const GrgPipeline = (
@@ -71,6 +75,41 @@ const redis = new import_ioredis.default(process.env.REDIS_URL ?? "redis://127.0
71
75
  });
72
76
  redis.on("error", () => {
73
77
  });
78
+ const REPLAY_TTL_SECONDS = Number.parseInt(process.env.TTTPS_REPLAY_TTL_SECONDS ?? "86400", 10);
79
+ function requiredFlag(name) {
80
+ return process.env[name]?.trim() === "1";
81
+ }
82
+ function configuredFreshnessPolicy() {
83
+ const raw = process.env.TTTPS_V08_MAX_SKEW_NS?.trim();
84
+ if (!raw) return void 0;
85
+ try {
86
+ const maxSkewNs = BigInt(raw);
87
+ if (maxSkewNs < 0n) return void 0;
88
+ return { nowNs: BigInt(Date.now()) * 1000000n, maxSkewNs };
89
+ } catch {
90
+ return void 0;
91
+ }
92
+ }
93
+ async function claimV2Replay(ctxIdHex, nonceHex) {
94
+ if (!ctxIdHex || !nonceHex) return "unavailable";
95
+ const key = `tttps:v2:replay:${ctxIdHex}:${nonceHex}`;
96
+ try {
97
+ const result = await redis.set(key, "1", "EX", REPLAY_TTL_SECONDS, "NX");
98
+ return result === "OK" ? "claimed" : "replay";
99
+ } catch {
100
+ return "unavailable";
101
+ }
102
+ }
103
+ async function claimV08Replay(clientId, sessionId, nonceHex) {
104
+ if (!clientId || !sessionId || !nonceHex) return "unavailable";
105
+ const key = `tttps:v08:replay:${clientId}:${sessionId}:${nonceHex}`;
106
+ try {
107
+ const result = await redis.set(key, "1", "EX", REPLAY_TTL_SECONDS, "NX");
108
+ return result === "OK" ? "claimed" : "replay";
109
+ } catch {
110
+ return "unavailable";
111
+ }
112
+ }
74
113
  const timeSynth = new import_openttt.TimeSynthesis();
75
114
  const adaptiveSwitch = new import_openttt.AdaptiveSwitch();
76
115
  const potSigner = new import_openttt.PotSigner();
@@ -374,17 +413,104 @@ async function potVerify(args) {
374
413
  verifiedAt: Date.now()
375
414
  });
376
415
  }
416
+ async function potGenerateV2(args) {
417
+ (0, import_telemetry.telemetryIncrement)("pot_generate_v2");
418
+ const ctxId = Buffer.from(args.ctxId, "hex");
419
+ const holderAuthData = Buffer.from(args.holderAuthData, "hex");
420
+ if (ctxId.length !== 16) throw new Error("ctxId must be exactly 16 bytes of hex");
421
+ if (holderAuthData.length !== 32) throw new Error("holderAuthData must be exactly 32 bytes of hex");
422
+ const tsTaiUs = BigInt(args.tsTaiUs);
423
+ const nonce = (0, import_crypto.randomBytes)(16);
424
+ const issuerKeyId = (0, import_crypto.createHash)("sha256").update(potSignerPublicKeyRawV08).digest().readUInt32BE(0);
425
+ const record = (0, import_pot_record_v2.encodePotRecordV2)({
426
+ holderAuthType: args.holderAuthType ?? 1,
427
+ algId: 1,
428
+ tsTaiUs,
429
+ dispersionUs: args.dispersionUs,
430
+ ctxId,
431
+ nonce,
432
+ holderAuthData,
433
+ issuerKeyId
434
+ }, potSignerPrivateKeyV08);
435
+ return serialize({
436
+ wireProfile: "draft-helmprotocol-tttps-11-v2",
437
+ potRecordV2: record.toString("hex"),
438
+ issuerPubKey: potSignerPublicKeyRawV08.toString("hex"),
439
+ issuerKeyId,
440
+ nonce: nonce.toString("hex"),
441
+ bindingProofRequired: process.env.TTTPS_V2_REQUIRE_BINDING === "1"
442
+ });
443
+ }
444
+ async function potVerifyV2(args) {
445
+ (0, import_telemetry.telemetryIncrement)("pot_verify_v2");
446
+ const bindingRequired = process.env.TTTPS_V2_REQUIRE_BINDING === "1";
447
+ if (bindingRequired && !args.bindingProof) {
448
+ return serialize({ verdict: "rejected", reason: "TLS exporter binding proof is required" });
449
+ }
450
+ const record = Buffer.from(args.potRecordV2, "hex");
451
+ const issuerPubKey = args.issuerPubKey ? Buffer.from(args.issuerPubKey, "hex") : potSignerPublicKeyRawV08;
452
+ const freshness = args.nowTaiUs !== void 0 && args.maxSkewUs !== void 0 ? { nowTaiUs: BigInt(args.nowTaiUs), maxSkewUs: BigInt(args.maxSkewUs) } : void 0;
453
+ const result = (0, import_pot_record_v2.verifyPotRecordV2)(record, issuerPubKey, freshness);
454
+ if (result.verdict === "rejected") return serialize(result);
455
+ if (args.bindingProof) {
456
+ const exporter = (0, import_transport_context.currentTransportBinding)()?.exporter;
457
+ if (!exporter) return serialize({ verdict: "rejected", reason: "live TLS exporter unavailable" });
458
+ let exporterOutput;
459
+ try {
460
+ exporterOutput = exporter((0, import_pot_record_v2.computeV2ExporterContext)(record));
461
+ } catch {
462
+ return serialize({ verdict: "rejected", reason: "TLS exporter derivation failed" });
463
+ }
464
+ const binding = (0, import_pot_record_v2.verifyV2BindingProof)(record, Buffer.from(args.bindingProof, "hex"), exporterOutput);
465
+ if (binding.verdict === "rejected") return serialize(binding);
466
+ }
467
+ if (process.env.TTTPS_REQUIRE_REPLAY_LEDGER === "1") {
468
+ const ctxId = record.subarray(16, 32).toString("hex");
469
+ const replay = await claimV2Replay(ctxId, result.nonce.toString("hex"));
470
+ if (replay !== "claimed") return serialize({ verdict: "rejected", reason: replay === "replay" ? "replay detected" : "replay ledger unavailable" });
471
+ }
472
+ return serialize({ verdict: "intact", wireProfile: "draft-helmprotocol-tttps-11-v2", nonce: result.nonce.toString("hex") });
473
+ }
377
474
  async function potVerifyV08(args) {
378
475
  (0, import_telemetry.telemetryIncrement)("pot_verify_v08");
379
476
  const record = Buffer.from(args.potRecordV08, "hex");
380
477
  const ctxId = args.ctxId ?? DEFAULT_CTX_ID_V08;
381
478
  const issuerPubKey = args.issuerPubKey !== void 0 ? Buffer.from(args.issuerPubKey, "hex") : potSignerPublicKeyRawV08;
382
479
  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);
480
+ const requireFreshness = requiredFlag("TTTPS_REQUIRE_V08_FRESHNESS");
481
+ const freshness = configuredFreshnessPolicy();
482
+ if (requireFreshness && freshness === void 0) {
483
+ return serialize({ verdict: "rejected", reason: "freshness policy is not configured", ctxId, verifiedAt: Date.now() });
484
+ }
485
+ const result = (0, import_pot_record_v08.verifyPotRecordV08)(record, ctxId, issuerPubKey, content, freshness);
486
+ if (result.verdict === "rejected") {
487
+ return serialize({
488
+ verdict: result.verdict,
489
+ reason: result.reason ?? null,
490
+ payloadDigestMatchesContent: result.payloadDigestMatchesContent ?? null,
491
+ ctxId,
492
+ verifiedAt: Date.now()
493
+ });
494
+ }
495
+ let replayClaim = "not_required";
496
+ if (requiredFlag("TTTPS_REQUIRE_REPLAY_LEDGER")) {
497
+ const decoded = (0, import_pot_record_v08.decodePotRecordV08)(record);
498
+ replayClaim = await claimV08Replay(args.clientId ?? "", args.sessionId ?? "", decoded.nonce.toString("hex"));
499
+ if (replayClaim !== "claimed") {
500
+ return serialize({
501
+ verdict: "rejected",
502
+ reason: replayClaim === "replay" ? "replay detected" : "replay ledger unavailable",
503
+ replayClaim,
504
+ ctxId,
505
+ verifiedAt: Date.now()
506
+ });
507
+ }
508
+ }
384
509
  return serialize({
385
510
  verdict: result.verdict,
386
511
  reason: result.reason ?? null,
387
512
  payloadDigestMatchesContent: result.payloadDigestMatchesContent ?? null,
513
+ replayClaim,
388
514
  ctxId,
389
515
  verifiedAt: Date.now()
390
516
  });
@@ -645,12 +771,14 @@ async function potCheckpoint(args) {
645
771
  0 && (module.exports = {
646
772
  potCheckpoint,
647
773
  potGenerate,
774
+ potGenerateV2,
648
775
  potGraph,
649
776
  potHealth,
650
777
  potQuery,
651
778
  potStats,
652
779
  potVerify,
653
780
  potVerifyV08,
781
+ potVerifyV2,
654
782
  redis,
655
783
  restoreDagEntry,
656
784
  tttsFreshnessSeal,
@@ -0,0 +1,37 @@
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 transport_context_exports = {};
20
+ __export(transport_context_exports, {
21
+ currentTransportBinding: () => currentTransportBinding,
22
+ withTransportBinding: () => withTransportBinding
23
+ });
24
+ module.exports = __toCommonJS(transport_context_exports);
25
+ var import_node_async_hooks = require("node:async_hooks");
26
+ const storage = new import_node_async_hooks.AsyncLocalStorage();
27
+ function withTransportBinding(context, fn) {
28
+ return storage.run(context, fn);
29
+ }
30
+ function currentTransportBinding() {
31
+ return storage.getStore();
32
+ }
33
+ // Annotate the CommonJS export names for ESM import in node:
34
+ 0 && (module.exports = {
35
+ currentTransportBinding,
36
+ withTransportBinding
37
+ });
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@helm-protocol/ttt-mcp",
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",
3
+ "version": "0.4.0",
4
+ "description": "Proof-of-Time attestation 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
- "ttt-mcp": "./dist/index.js"
7
+ "ttt-mcp": "dist/index.js"
8
8
  },
9
9
  "files": [
10
10
  "dist",
11
11
  "README.md"
12
12
  ],
13
13
  "scripts": {
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",
14
+ "build": "esbuild index.ts tools.ts telemetry.ts auth.ts server.ts pot_record_v08.ts pot_record_v2.ts tls_exporter.ts transport_context.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",
@@ -50,7 +50,7 @@
50
50
  "license": "BSL-1.1",
51
51
  "repository": {
52
52
  "type": "git",
53
- "url": "https://github.com/Helm-Protocol/openttt-mcp"
53
+ "url": "git+https://github.com/Helm-Protocol/openttt-mcp.git"
54
54
  },
55
55
  "homepage": "https://github.com/Helm-Protocol/openttt-mcp#readme",
56
56
  "engines": {
@@ -59,7 +59,7 @@
59
59
  "dependencies": {
60
60
  "@modelcontextprotocol/sdk": "^1.27.1",
61
61
  "ioredis": "^5.11.0",
62
- "openttt": "^0.2.13",
62
+ "openttt": "^0.3.1",
63
63
  "zod": "^3.25.0"
64
64
  },
65
65
  "devDependencies": {
@@ -70,5 +70,8 @@
70
70
  "ts-jest": "^29.4.11",
71
71
  "typescript": "^5.3.3"
72
72
  },
73
- "mcpName": "io.github.Helm-Protocol/openttt-pot"
73
+ "mcpName": "io.github.Helm-Protocol/openttt-pot",
74
+ "publishConfig": {
75
+ "access": "public"
76
+ }
74
77
  }