Skip to content

Commit 38386a6

Browse files
authored
chore(release): v2026.3.2.1 (#7)
* fix: security hardening and cleanup - Add checksum verification to install.sh (SHA256, optional strict mode) - Fix regex injection in envOrDotEnv() via key escaping (billing.ts, mcp.ts) - Use timing-safe comparison for bearer token auth (mcp.ts) - Remove unused --system flag from analyze command - Deduplicate SKILL.md config fields * chore(release): v2026.3.2.1
1 parent 1006b62 commit 38386a6

6 files changed

Lines changed: 55 additions & 17 deletions

File tree

SKILL.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,7 @@ credentials:
2828
required: false
2929
required_env_vars:
3030
- X_BEARER_TOKEN
31-
requiredEnvVars:
32-
- X_BEARER_TOKEN
3331
primary_credential: X_BEARER_TOKEN
34-
primaryCredential: X_BEARER_TOKEN
3532
security:
3633
always: false
3734
autonomous: false

install.sh

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,49 @@ main() {
4545
local tarball="${tmpdir}/xint.tar.gz"
4646
curl -fsSL "$tarball_url" -o "$tarball"
4747

48+
# Checksum verification
49+
local checksums_url="https://github.com/${OWNER}/${REPO}/releases/download/${version}/checksums.txt"
50+
local checksums_file="${tmpdir}/checksums.txt"
51+
if curl -fsSL "$checksums_url" -o "$checksums_file" 2>/dev/null; then
52+
local asset_name
53+
asset_name="$(basename "$tarball_url")"
54+
local expected
55+
expected="$(awk -v name="$asset_name" '$0 ~ name {print $1; exit}' "$checksums_file" || true)"
56+
if [[ -n "$expected" ]]; then
57+
local actual=""
58+
if command -v sha256sum >/dev/null 2>&1; then
59+
actual="$(sha256sum "$tarball" | awk '{print $1}')"
60+
elif command -v shasum >/dev/null 2>&1; then
61+
actual="$(shasum -a 256 "$tarball" | awk '{print $1}')"
62+
fi
63+
if [[ -n "$actual" ]]; then
64+
if [[ "$actual" != "$expected" ]]; then
65+
echo "error: checksum mismatch for $asset_name" >&2
66+
exit 1
67+
fi
68+
echo "==> Checksum verified"
69+
else
70+
if [[ "${XINT_INSTALL_REQUIRE_CHECKSUM:-0}" == "1" ]]; then
71+
echo "error: checksum required but neither sha256sum nor shasum is available" >&2
72+
exit 1
73+
fi
74+
echo "==> Checksum tool unavailable; skipping verification"
75+
fi
76+
else
77+
if [[ "${XINT_INSTALL_REQUIRE_CHECKSUM:-0}" == "1" ]]; then
78+
echo "error: checksum required but no entry found in checksums.txt" >&2
79+
exit 1
80+
fi
81+
echo "==> Checksums file present but no entry for $asset_name; skipping verification"
82+
fi
83+
else
84+
if [[ "${XINT_INSTALL_REQUIRE_CHECKSUM:-0}" == "1" ]]; then
85+
echo "error: checksum required but checksums.txt not found in release" >&2
86+
exit 1
87+
fi
88+
echo "==> No checksums.txt in release; skipping checksum verification"
89+
fi
90+
4891
echo "==> Extracting"
4992
tar -xzf "$tarball" -C "$tmpdir"
5093
local src_dir

lib/billing.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ function envOrDotEnv(key: string): string | undefined {
1111
try {
1212
const envPath = join(import.meta.dir, "..", ".env");
1313
const raw = readFileSync(envPath, "utf-8");
14-
const match = raw.match(new RegExp(`^${key}=["']?([^"'\\n]+)`, "m"));
14+
const safeKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15+
const match = raw.match(new RegExp(`^${safeKey}=["']?([^"'\\n]+)`, "m"));
1516
if (match?.[1]) return match[1].trim();
1617
} catch {
1718
// no-op; fall back to runtime env/defaults

lib/grok.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,6 @@ function estimateCost(
359359

360360
export async function cmdAnalyze(args: string[]): Promise<void> {
361361
let model = DEFAULT_MODEL;
362-
let systemPrompt: string | undefined;
363362
let tweetFile: string | undefined;
364363
let pipeMode = false;
365364
let imageUrl: string | undefined;
@@ -377,13 +376,6 @@ export async function cmdAnalyze(args: string[]): Promise<void> {
377376
process.exit(1);
378377
}
379378
break;
380-
case "--system":
381-
systemPrompt = args[++i];
382-
if (!systemPrompt) {
383-
console.error("Error: --system requires a prompt string");
384-
process.exit(1);
385-
}
386-
break;
387379
case "--tweets":
388380
tweetFile = args[++i];
389381
if (!tweetFile) {
@@ -447,7 +439,7 @@ export async function cmdAnalyze(args: string[]): Promise<void> {
447439
const messages: GrokMessage[] = [
448440
{
449441
role: "system",
450-
content: systemPrompt || GENERAL_ANALYST_SYSTEM,
442+
content: GENERAL_ANALYST_SYSTEM,
451443
},
452444
{ role: "user", content: query },
453445
];
@@ -526,7 +518,6 @@ Usage: xint analyze <query> Ask Grok a question
526518
527519
Options:
528520
--model <name> Model: grok-3, grok-3-mini (default), grok-2, grok-2-vision
529-
--system <prompt> Custom system prompt
530521
--tweets <file> Path to JSON file containing tweets
531522
--pipe Read tweet JSON from stdin
532523
--image, -i <url> Image URL to analyze with Grok Vision

lib/mcp.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { checkBudget } from "./costs";
99
import { recordCommandResult } from "./reliability";
1010
import { readFileSync } from "fs";
1111
import { join } from "path";
12+
import { timingSafeEqual } from "crypto";
1213
import { createMcpToolHandlers, type MCPToolHandler, type ToolExecutionResult } from "./mcp_dispatcher";
1314

1415
type PolicyMode = "read_only" | "engagement" | "moderation";
@@ -38,7 +39,8 @@ function envOrDotEnv(key: string): string | undefined {
3839
try {
3940
const envPath = join(import.meta.dir, "..", ".env");
4041
const raw = readFileSync(envPath, "utf-8");
41-
const m = raw.match(new RegExp(`^${key}=["']?([^"'\\n]+)`, "m"));
42+
const safeKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
43+
const m = raw.match(new RegExp(`^${safeKey}=["']?([^"'\\n]+)`, "m"));
4244
if (m?.[1]) return m[1].trim();
4345
} catch {}
4446
return undefined;
@@ -664,7 +666,11 @@ async function runStdio(options: MCPServerOptions) {
664666

665667
function hasValidBearerToken(authHeader: string | undefined, expectedToken: string): boolean {
666668
if (!authHeader || !authHeader.startsWith("Bearer ")) return false;
667-
return authHeader.slice("Bearer ".length).trim() === expectedToken;
669+
const provided = authHeader.slice("Bearer ".length).trim();
670+
const a = Buffer.from(provided);
671+
const b = Buffer.from(expectedToken);
672+
if (a.length !== b.length) return false;
673+
return timingSafeEqual(a, b);
668674
}
669675

670676
async function runSSE(port: number, options: MCPSSEServerOptions) {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "xint",
3-
"version": "2026.2.24",
3+
"version": "2026.3.2.1",
44
"author": "Nyk",
55
"repository": {
66
"type": "git",

0 commit comments

Comments
 (0)