sigrank 0.0.175 → 0.0.177

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
@@ -147,7 +147,7 @@ smithery tool call sigrank rank_paste '{"text": "1000000 500000 50000 800000"}'
147
147
  ## Commands
148
148
 
149
149
  ```
150
- ⊙ SigRank CLI v0.0.175
150
+ ⊙ SigRank CLI v0.0.177
151
151
 
152
152
  Default (no args)
153
153
  sigrank unified dashboard: cascade + token pillars + board
package/cli.mjs CHANGED
@@ -236,7 +236,10 @@ async function fetchBoard(window = "30d") {
236
236
  const res = await fetch(
237
237
  `${DEFAULT_API_BASE}/api/v1/leaderboard?window=${window}&metric=yield_`,
238
238
  {
239
- headers: { accept: "application/json" },
239
+ headers: {
240
+ accept: "application/json",
241
+ "user-agent": `node/${process.version} sigrank-mcp/cli`,
242
+ },
240
243
  },
241
244
  );
242
245
  if (!res.ok) throw new Error(`Board API → HTTP ${res.status}`);
@@ -1472,6 +1475,7 @@ async function runSigRank() {
1472
1475
  headers: {
1473
1476
  "content-type": "application/json",
1474
1477
  accept: "application/json",
1478
+ "user-agent": `node/${process.version} sigrank-mcp/cli`,
1475
1479
  },
1476
1480
  body: JSON.stringify({
1477
1481
  codename,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigrank",
3
- "version": "0.0.175",
3
+ "version": "0.0.177",
4
4
  "author": "SunrisesIllNeverSee (https://github.com/SunrisesIllNeverSee)",
5
5
  "description": "SigRank MCP server — the yield cascade + live leaderboard as MCP tools any agent can call",
6
6
  "mcpName": "io.github.SunrisesIllNeverSee/sigrank-mcp",
package/submit.mjs CHANGED
@@ -11,9 +11,22 @@
11
11
 
12
12
  import { snapshotHash, signPayload } from "./sign.mjs";
13
13
  import { preflight } from "./preflight.mjs";
14
+ import { readFileSync } from "node:fs";
14
15
 
15
16
  const WINDOW_TYPE = { "7d": "7d", "30d": "30d", "90d": "90d", all: "all_time" };
16
17
  const WINDOW_SPAN_DAYS = { "7d": 7, "30d": 30, "90d": 90, all_time: 3650 };
18
+
19
+ /** Resolve this package's version for the User-Agent stamp (best-effort). */
20
+ function _pkgVersion() {
21
+ try {
22
+ const pkg = JSON.parse(
23
+ readFileSync(new URL("./package.json", import.meta.url), "utf-8"),
24
+ );
25
+ return pkg.version;
26
+ } catch {
27
+ return "unknown";
28
+ }
29
+ }
17
30
  const PLATFORM_ENUM = new Set([
18
31
  "claude",
19
32
  "chatgpt",
@@ -181,8 +194,13 @@ export async function submitSignedWindow(
181
194
  ((url, init = {}) => {
182
195
  const ctrl = new AbortController();
183
196
  const timer = setTimeout(() => ctrl.abort(), 15_000);
197
+ const headers = {
198
+ "user-agent": `node/${process.version} sigrank-mcp/${_pkgVersion()}`,
199
+ ...(init.headers || {}),
200
+ };
184
201
  return fetch(url, {
185
202
  ...init,
203
+ headers,
186
204
  signal: init.signal || ctrl.signal,
187
205
  }).finally(() => clearTimeout(timer));
188
206
  });
package/tools.mjs CHANGED
@@ -26,7 +26,7 @@ import { ALL_PLATFORMS } from "./adapters.mjs";
26
26
  import { ensureIdentity, recordEnrollment } from "./keystore.mjs";
27
27
  import { submitSignedWindow } from "./submit.mjs";
28
28
  import { createHash } from "node:crypto";
29
- import { execFile } from "node:child_process";
29
+ import { execFile, execFileSync } from "node:child_process";
30
30
  import { existsSync, readFileSync } from "node:fs";
31
31
  import os from "node:os";
32
32
  import path from "node:path";
@@ -256,6 +256,56 @@ export const DEFAULT_FETCH_TIMEOUT =
256
256
  * past this is malformed or abusive — reject cleanly before parsing/POSTing (E2 hardening). */
257
257
  const MAX_INPUT = 1_000_000;
258
258
 
259
+ /** Resolve this package's version for the User-Agent stamp (best-effort). */
260
+ function agentVersionStamp() {
261
+ try {
262
+ const pkg = JSON.parse(
263
+ readFileSync(new URL("./package.json", import.meta.url), "utf-8"),
264
+ );
265
+ return pkg.version;
266
+ } catch {
267
+ return "unknown";
268
+ }
269
+ }
270
+
271
+ /**
272
+ * curl-based fetch fallback for when Node's fetch is blocked by Vercel's bot
273
+ * protection (TLS fingerprinting). Returns a Response-like object with .ok,
274
+ * .status, .json(), .text(). Uses execFileSync for synchronous curl execution.
275
+ */
276
+ function curlFetch(url, init = {}, timeoutMs = 10_000) {
277
+ const args = ["-s", "-S", "-w", "\n__HTTP_STATUS__%{http_code}", "--max-time", String(Math.ceil(timeoutMs / 1000))];
278
+ if (init.method) args.push("-X", init.method);
279
+ const headers = init.headers || {};
280
+ for (const [k, v] of Object.entries(headers)) {
281
+ args.push("-H", `${k}: ${v}`);
282
+ }
283
+ if (init.body) {
284
+ args.push("-d", init.body);
285
+ }
286
+ args.push(url);
287
+ let stdout;
288
+ try {
289
+ stdout = execFileSync("curl", args, {
290
+ encoding: "utf-8",
291
+ maxBuffer: 10 * 1024 * 1024,
292
+ timeout: timeoutMs + 2000,
293
+ });
294
+ } catch (e) {
295
+ throw new Error(`curl transport failed: ${e.message}`);
296
+ }
297
+ // Parse the status code from the trailer
298
+ const statusMatch = stdout.match(/__HTTP_STATUS__(\d+)$/);
299
+ const status = statusMatch ? parseInt(statusMatch[1], 10) : 0;
300
+ const bodyText = statusMatch ? stdout.slice(0, statusMatch.index) : stdout;
301
+ return {
302
+ ok: status >= 200 && status < 300,
303
+ status,
304
+ json: async () => { try { return JSON.parse(bodyText); } catch { return {}; } },
305
+ text: async () => bodyText,
306
+ };
307
+ }
308
+
259
309
  // ── Smithery quality: output schemas + annotations ──────────────────────────
260
310
  // MCP tool annotations hint to clients about side-effects, read-only status, etc.
261
311
  const ANNOTATIONS = {
@@ -1357,13 +1407,45 @@ export async function callTool(name, args, opts = {}) {
1357
1407
  const rawFetch = opts.fetchImpl || fetch;
1358
1408
 
1359
1409
  // Wrap every fetch with an AbortController timeout so a hung network call never
1360
- // blocks the MCP client indefinitely.
1361
- const doFetch = (url, init = {}) => {
1410
+ // blocks the MCP client indefinitely. Vercel's bot protection (security
1411
+ // checkpoint) blocks Node's fetch via TLS fingerprinting — no UA fix helps.
1412
+ // When fetch gets a 403 Vercel challenge, fall back to curl (which passes).
1413
+ const doFetch = async (url, init = {}) => {
1362
1414
  const ac = new AbortController();
1363
1415
  const timer = setTimeout(() => ac.abort(), timeoutMs);
1364
- return rawFetch(url, { ...init, signal: ac.signal }).finally(() =>
1365
- clearTimeout(timer),
1366
- );
1416
+ const headers = {
1417
+ "user-agent": `node/${process.version} sigrank-mcp/${agentVersionStamp()}`,
1418
+ ...(init.headers || {}),
1419
+ };
1420
+ try {
1421
+ const res = await rawFetch(url, { ...init, headers, signal: ac.signal });
1422
+ // Detect Vercel security checkpoint (403 + HTML body, not JSON)
1423
+ if (res.status === 403) {
1424
+ const text = await res.text();
1425
+ if (text.includes("Vercel Security Checkpoint") || text.includes("x-vercel-challenge")) {
1426
+ // Fall back to curl transport
1427
+ return curlFetch(url, init, timeoutMs);
1428
+ }
1429
+ // Real 403 from the API — return a mock response object
1430
+ return {
1431
+ ok: false,
1432
+ status: 403,
1433
+ json: async () => { try { return JSON.parse(text); } catch { return {}; } },
1434
+ text: async () => text,
1435
+ };
1436
+ }
1437
+ return res;
1438
+ } catch (e) {
1439
+ if (e.name === "AbortError") throw e;
1440
+ // Network error — try curl as fallback
1441
+ try {
1442
+ return await curlFetch(url, init, timeoutMs);
1443
+ } catch {
1444
+ throw e;
1445
+ }
1446
+ } finally {
1447
+ clearTimeout(timer);
1448
+ }
1367
1449
  };
1368
1450
 
1369
1451
  const fetchJson = async (path) => {