@helm-protocol/ttt-mcp 0.1.7 → 0.1.9

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 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
+ });