@helm-protocol/ttt-mcp 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.js +257 -0
- package/dist/index.js +22498 -24
- package/dist/tools.js +8 -6
- package/package.json +3 -3
package/dist/auth.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var auth_exports = {};
|
|
30
|
+
__export(auth_exports, {
|
|
31
|
+
AUTH_ERROR_MESSAGE: () => AUTH_ERROR_MESSAGE,
|
|
32
|
+
verifyHelmApiKey: () => verifyHelmApiKey
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(auth_exports);
|
|
35
|
+
var import_crypto = require("crypto");
|
|
36
|
+
var https = __toESM(require("https"));
|
|
37
|
+
var import_crypto2 = require("crypto");
|
|
38
|
+
const HELM_PUBLIC_KEY_B64 = process.env.HELM_PUBLIC_KEY ?? "PLACEHOLDER_REPLACE_BEFORE_DEPLOY";
|
|
39
|
+
const HELM_AUTH_SERVER_URL = process.env.HELM_AUTH_SERVER_URL;
|
|
40
|
+
if (HELM_AUTH_SERVER_URL !== void 0) {
|
|
41
|
+
if (!HELM_AUTH_SERVER_URL.startsWith("https://")) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
"[ttt-mcp] HELM_AUTH_SERVER_URL must use HTTPS (got: " + HELM_AUTH_SERVER_URL + "). Refusing to start to protect API keys in transit."
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const CLIENT_VERSION = "0.2.0";
|
|
48
|
+
const authCache = /* @__PURE__ */ new Map();
|
|
49
|
+
function getCached(apiKey, toolName) {
|
|
50
|
+
const key = `${apiKey}:${toolName}`;
|
|
51
|
+
const entry = authCache.get(key);
|
|
52
|
+
if (!entry) return null;
|
|
53
|
+
if (Date.now() > entry.expiresAt) {
|
|
54
|
+
authCache.delete(key);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return entry;
|
|
58
|
+
}
|
|
59
|
+
function setCached(apiKey, toolName, allowed, planTier, cacheTtlMs) {
|
|
60
|
+
const key = `${apiKey}:${toolName}`;
|
|
61
|
+
authCache.set(key, {
|
|
62
|
+
allowed,
|
|
63
|
+
planTier,
|
|
64
|
+
expiresAt: Date.now() + cacheTtlMs
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const CIRCUIT_FAILURE_THRESHOLD = 3;
|
|
68
|
+
const CIRCUIT_OPEN_DURATION_MS = 5 * 60 * 1e3;
|
|
69
|
+
const circuit = {
|
|
70
|
+
state: "CLOSED",
|
|
71
|
+
consecutiveFailures: 0,
|
|
72
|
+
openedAt: 0
|
|
73
|
+
};
|
|
74
|
+
function circuitRecordSuccess() {
|
|
75
|
+
circuit.consecutiveFailures = 0;
|
|
76
|
+
circuit.state = "CLOSED";
|
|
77
|
+
}
|
|
78
|
+
function circuitRecordFailure() {
|
|
79
|
+
circuit.consecutiveFailures += 1;
|
|
80
|
+
if (circuit.consecutiveFailures >= CIRCUIT_FAILURE_THRESHOLD) {
|
|
81
|
+
circuit.state = "OPEN";
|
|
82
|
+
circuit.openedAt = Date.now();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function circuitCanAttempt() {
|
|
86
|
+
if (circuit.state === "CLOSED") return true;
|
|
87
|
+
if (circuit.state === "OPEN") {
|
|
88
|
+
if (Date.now() - circuit.openedAt >= CIRCUIT_OPEN_DURATION_MS) {
|
|
89
|
+
circuit.state = "HALF_OPEN";
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
function callAuthServer(serverUrl, apiKey, toolName) {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const requestId = (0, import_crypto2.randomUUID)();
|
|
99
|
+
const body = JSON.stringify({
|
|
100
|
+
api_key: apiKey,
|
|
101
|
+
tool_name: toolName,
|
|
102
|
+
request_id: requestId,
|
|
103
|
+
timestamp_ms: Date.now(),
|
|
104
|
+
client_version: CLIENT_VERSION
|
|
105
|
+
});
|
|
106
|
+
const url = new URL("/v1/auth/verify", serverUrl);
|
|
107
|
+
const options = {
|
|
108
|
+
hostname: url.hostname,
|
|
109
|
+
port: url.port || 443,
|
|
110
|
+
path: url.pathname,
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: {
|
|
113
|
+
"Content-Type": "application/json",
|
|
114
|
+
"Content-Length": Buffer.byteLength(body),
|
|
115
|
+
"X-Request-ID": requestId
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const req = https.request(options, (res) => {
|
|
119
|
+
let data = "";
|
|
120
|
+
res.on("data", (chunk) => {
|
|
121
|
+
data += chunk.toString();
|
|
122
|
+
});
|
|
123
|
+
res.on("end", () => {
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(data);
|
|
126
|
+
resolve(parsed);
|
|
127
|
+
} catch {
|
|
128
|
+
reject(new Error("Auth server returned non-JSON response"));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
req.setTimeout(3e3, () => {
|
|
133
|
+
req.destroy(new Error("Auth server request timed out (3000ms)"));
|
|
134
|
+
});
|
|
135
|
+
req.on("error", (err) => {
|
|
136
|
+
reject(err);
|
|
137
|
+
});
|
|
138
|
+
req.write(body);
|
|
139
|
+
req.end();
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async function serverSideGate(apiKey, toolName) {
|
|
143
|
+
if (!HELM_AUTH_SERVER_URL) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const cached = getCached(apiKey, toolName);
|
|
147
|
+
if (cached !== null) {
|
|
148
|
+
if (!cached.allowed) {
|
|
149
|
+
return `Access denied by Helm Auth Server (plan: ${cached.planTier}, cached)`;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
if (!circuitCanAttempt()) {
|
|
154
|
+
return "Helm Auth Server is temporarily unreachable. Access denied (fail-closed). Please retry in a few minutes.";
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const resp = await callAuthServer(HELM_AUTH_SERVER_URL, apiKey, toolName);
|
|
158
|
+
const ttl = typeof resp.cache_ttl_ms === "number" && resp.cache_ttl_ms > 0 ? resp.cache_ttl_ms : 3e5;
|
|
159
|
+
setCached(apiKey, toolName, resp.allowed, resp.plan_tier ?? "unknown", ttl);
|
|
160
|
+
circuitRecordSuccess();
|
|
161
|
+
if (!resp.allowed) {
|
|
162
|
+
const reason = resp.reason ? ` Reason: ${resp.reason}` : "";
|
|
163
|
+
return `Access denied by Helm Auth Server (plan: ${resp.plan_tier}).${reason}`;
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
} catch {
|
|
167
|
+
circuitRecordFailure();
|
|
168
|
+
return "Helm Auth Server is unreachable. Access denied (fail-closed). Please retry later.";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function verifyHelmApiKey(apiKey, opts) {
|
|
172
|
+
if (!apiKey) {
|
|
173
|
+
return { valid: false, error: "No API key provided" };
|
|
174
|
+
}
|
|
175
|
+
if (!apiKey.startsWith("hk-")) {
|
|
176
|
+
return { valid: false, error: "Invalid key format" };
|
|
177
|
+
}
|
|
178
|
+
if (HELM_PUBLIC_KEY_B64 === "PLACEHOLDER_REPLACE_BEFORE_DEPLOY") {
|
|
179
|
+
if (process.env.HELM_DEV_MODE === "true") {
|
|
180
|
+
return { valid: true, tier: 0, orgId: "dev-mode" };
|
|
181
|
+
}
|
|
182
|
+
return { valid: false, error: "Server misconfiguration: HELM_PUBLIC_KEY not set" };
|
|
183
|
+
}
|
|
184
|
+
const jwt = apiKey.slice(3);
|
|
185
|
+
const parts = jwt.split(".");
|
|
186
|
+
if (parts.length !== 3) {
|
|
187
|
+
return { valid: false, error: "Malformed JWT" };
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const payload = JSON.parse(
|
|
191
|
+
Buffer.from(parts[1], "base64url").toString("utf8")
|
|
192
|
+
);
|
|
193
|
+
if (typeof payload.exp !== "number" || payload.exp < Math.floor(Date.now() / 1e3)) {
|
|
194
|
+
return { valid: false, error: "Key expired" };
|
|
195
|
+
}
|
|
196
|
+
if (payload.iss !== "helm-protocol") {
|
|
197
|
+
return { valid: false, error: "Invalid issuer" };
|
|
198
|
+
}
|
|
199
|
+
const publicKey = (0, import_crypto.createPublicKey)({
|
|
200
|
+
key: Buffer.from(HELM_PUBLIC_KEY_B64, "base64"),
|
|
201
|
+
format: "der",
|
|
202
|
+
type: "spki"
|
|
203
|
+
});
|
|
204
|
+
const sigInput = Buffer.from(`${parts[0]}.${parts[1]}`);
|
|
205
|
+
const sig = Buffer.from(parts[2], "base64url");
|
|
206
|
+
const isValid = (0, import_crypto.verify)(null, sigInput, publicKey, sig);
|
|
207
|
+
if (!isValid) {
|
|
208
|
+
return { valid: false, error: "Invalid signature" };
|
|
209
|
+
}
|
|
210
|
+
if (opts?.redisClient && payload.type === "short" && payload.jti) {
|
|
211
|
+
const redisKey = `helm:jti:${payload.jti}`;
|
|
212
|
+
const ttl = Math.max(
|
|
213
|
+
1,
|
|
214
|
+
payload.exp - Math.floor(Date.now() / 1e3)
|
|
215
|
+
);
|
|
216
|
+
const isNew = await opts.redisClient.set(redisKey, "1", {
|
|
217
|
+
NX: true,
|
|
218
|
+
EX: ttl
|
|
219
|
+
});
|
|
220
|
+
if (isNew === null) {
|
|
221
|
+
opts.onReplayDetected?.(payload.jti);
|
|
222
|
+
return { valid: false, error: "Replay detected" };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (opts?.toolName) {
|
|
226
|
+
const gateError = await serverSideGate(apiKey, opts.toolName);
|
|
227
|
+
if (gateError !== null) {
|
|
228
|
+
return { valid: false, error: gateError };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
valid: true,
|
|
233
|
+
tier: typeof payload.tier === "number" ? payload.tier : 0,
|
|
234
|
+
orgId: payload.sub
|
|
235
|
+
};
|
|
236
|
+
} catch {
|
|
237
|
+
return { valid: false, error: "Verification error" };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const AUTH_ERROR_MESSAGE = `This tool requires a Helm Protocol API key.
|
|
241
|
+
|
|
242
|
+
To get access:
|
|
243
|
+
1. Contact enterprise@helmprotocol.io
|
|
244
|
+
2. Add the key to your MCP config:
|
|
245
|
+
{ "env": { "HELM_API_KEY": "hk-..." } }
|
|
246
|
+
|
|
247
|
+
Optional server-side validation:
|
|
248
|
+
Set HELM_AUTH_SERVER_URL=https://auth.helm-protocol.com for additional
|
|
249
|
+
server-side gate (quota enforcement, revocation, plan tier checks).
|
|
250
|
+
HTTPS is required \u2014 HTTP URLs are rejected at startup.
|
|
251
|
+
|
|
252
|
+
Free tools available without a key: pot_health, pot_stats`;
|
|
253
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
254
|
+
0 && (module.exports = {
|
|
255
|
+
AUTH_ERROR_MESSAGE,
|
|
256
|
+
verifyHelmApiKey
|
|
257
|
+
});
|