Skip to content

Commit ec247d3

Browse files
tankcdrclaude
andcommitted
fix: P0 security fixes + P1 error handling, timeouts, rename cleanup
P0: isTrusted() fail-closed, SSRF URL validation on tweet/gist verification, Action cast validation in API + MCP, private: true on api/contracts packages. P1: score() throws instead of swallowing errors, bare catch blocks logged, CLI try/catch, MCP connect error handling, getERC8004Owner response check, 10s timeouts on all raw fetch() calls, engine test fixes (no wall-clock assertions), typecheck scripts for cli/mcp/web, evidenceURI https validation, rate-limit Map bounded to 10k entries, AegisConfig → TrstLyrConfig rename with deprecated alias, aegis-protocol → trstlyr-protocol User-Agent, [aegis] → [trstlyr] log prefix. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 987a2ae commit ec247d3

22 files changed

Lines changed: 279 additions & 110 deletions

File tree

apps/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "@trstlyr/api",
33
"version": "0.0.1",
44
"description": "TrstLyr HTTP API \u2014 Fastify adapter over @trstlyr/core",
5+
"private": true,
56
"license": "Apache-2.0",
67
"type": "module",
78
"main": "./dist/index.js",

apps/api/src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,14 @@ server.post('/v1/trust/gate', { config: { rateLimit: { max: 120, timeWindow: '1
574574
}
575575
if (!validateSubjectString(body.counterparty, reply)) return;
576576

577-
const action = (body.action ?? 'transact') as Action;
577+
const VALID_ACTIONS: Action[] = ['install', 'execute', 'delegate', 'transact', 'review'];
578+
const rawAction = body.action ?? 'transact';
579+
if (!VALID_ACTIONS.includes(rawAction as Action)) {
580+
return reply.code(400).send({
581+
error: `Invalid action "${rawAction}". Must be one of: ${VALID_ACTIONS.join(', ')}`,
582+
});
583+
}
584+
const action = rawAction as Action;
578585
const amountUsd = typeof body.amount_usd === 'number' ? body.amount_usd : null;
579586

580587
// Determine threshold based on action + amount

apps/api/src/routes/behavioral.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ export async function registerBehavioralRoutes(
6767
if (!interactionAt || typeof interactionAt !== 'number') {
6868
return reply.code(400).send({ error: '"interactionAt" is required (unix timestamp)' });
6969
}
70+
if (evidenceURI !== undefined && evidenceURI !== null) {
71+
try {
72+
const parsed = new URL(evidenceURI);
73+
if (parsed.protocol !== 'https:') {
74+
return reply.code(400).send({ error: '"evidenceURI" must use https:// scheme' });
75+
}
76+
} catch {
77+
return reply.code(400).send({ error: '"evidenceURI" must be a valid https:// URL' });
78+
}
79+
}
7080

7181
// Derive attester from x402 payment proof (EIP-3009 `from` = payer wallet)
7282
let attester: string | undefined;

apps/api/src/routes/discover.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,28 @@ const FALLBACK_SUBJECTS = [
8080
const discoverFreeTier = new Map<string, { count: number; resetAt: number }>();
8181
const FREE_LIMIT = 3;
8282
const DAY_MS = 86_400_000;
83+
const MAX_FREE_TIER_ENTRIES = 10_000;
8384

8485
function checkFreeQuota(ip: string): { allowed: boolean; remaining: number } {
8586
const now = Date.now();
87+
88+
// Prune expired entries and cap Map size to prevent unbounded growth
89+
if (discoverFreeTier.size > MAX_FREE_TIER_ENTRIES) {
90+
for (const [key, val] of discoverFreeTier) {
91+
if (now > val.resetAt) discoverFreeTier.delete(key);
92+
}
93+
// If still over limit after pruning expired, delete oldest entries
94+
if (discoverFreeTier.size > MAX_FREE_TIER_ENTRIES) {
95+
const excess = discoverFreeTier.size - MAX_FREE_TIER_ENTRIES;
96+
let deleted = 0;
97+
for (const key of discoverFreeTier.keys()) {
98+
if (deleted >= excess) break;
99+
discoverFreeTier.delete(key);
100+
deleted++;
101+
}
102+
}
103+
}
104+
86105
const entry = discoverFreeTier.get(ip);
87106

88107
if (!entry || now > entry.resetAt) {

apps/web/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"dev": "next dev -p 3001",
77
"build": "next build",
88
"start": "next start",
9-
"lint": "next lint"
9+
"lint": "next lint",
10+
"typecheck": "tsc --noEmit"
1011
},
1112
"dependencies": {
1213
"next": "^15.1.0",

contracts/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "@trstlyr/contracts",
33
"version": "0.0.1",
4+
"private": true,
45
"description": "TrstLyr on-chain components \u2014 EAS schema registration",
56
"license": "Apache-2.0",
67
"type": "module",

packages/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
],
1515
"scripts": {
1616
"build": "tsc",
17+
"typecheck": "tsc --noEmit",
1718
"prepublishOnly": "tsc"
1819
},
1920
"dependencies": {

packages/cli/src/index.ts

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,16 @@ ${c.bold('EXIT CODES')}
135135
// ── Command handlers ──
136136

137137
async function cmdScore(subject: string, json: boolean): Promise<void> {
138-
const result = await sdkScore(subject);
139-
if (json) {
140-
console.log(JSON.stringify(scoreJson(result), null, 2));
141-
} else {
142-
console.log(formatScore(result));
138+
try {
139+
const result = await sdkScore(subject);
140+
if (json) {
141+
console.log(JSON.stringify(scoreJson(result), null, 2));
142+
} else {
143+
console.log(formatScore(result));
144+
}
145+
} catch (err) {
146+
console.error(c.red(`Error scoring "${subject}": ${err instanceof Error ? err.message : String(err)}`));
147+
process.exit(1);
143148
}
144149
}
145150

@@ -175,11 +180,16 @@ async function cmdGate(subject: string, minScore: number, strict: boolean, json:
175180
}
176181

177182
async function cmdAttest(subject: string, json: boolean): Promise<void> {
178-
const result = await sdkAttest(subject);
179-
if (json) {
180-
console.log(JSON.stringify(result, null, 2));
181-
} else {
182-
console.log(formatAttestation(result));
183+
try {
184+
const result = await sdkAttest(subject);
185+
if (json) {
186+
console.log(JSON.stringify(result, null, 2));
187+
} else {
188+
console.log(formatAttestation(result));
189+
}
190+
} catch (err) {
191+
console.error(c.red(`Error attesting "${subject}": ${err instanceof Error ? err.message : String(err)}`));
192+
process.exit(1);
183193
}
184194
}
185195

@@ -190,22 +200,28 @@ async function cmdBehavioral(
190200
value: number | undefined,
191201
json: boolean,
192202
): Promise<void> {
193-
const result = await sdkBehavioral({
194-
subject,
195-
outcome: outcome as 'success' | 'partial' | 'failed',
196-
rating,
197-
value_usd: value,
198-
});
199-
if (json) {
200-
console.log(JSON.stringify(result, null, 2));
201-
} else {
202-
console.log(formatBehavioral(result));
203+
try {
204+
const result = await sdkBehavioral({
205+
subject,
206+
outcome: outcome as 'success' | 'partial' | 'failed',
207+
rating,
208+
value_usd: value,
209+
});
210+
if (json) {
211+
console.log(JSON.stringify(result, null, 2));
212+
} else {
213+
console.log(formatBehavioral(result));
214+
}
215+
} catch (err) {
216+
console.error(c.red(`Error posting behavioral attestation: ${err instanceof Error ? err.message : String(err)}`));
217+
process.exit(1);
203218
}
204219
}
205220

206221
// ── Main ──
207222

208223
export async function main(): Promise<void> {
224+
try {
209225
const { values, positionals } = parseArgs({
210226
args: process.argv.slice(2),
211227
options: {
@@ -271,4 +287,8 @@ export async function main(): Promise<void> {
271287
console.error(`Unknown command: ${command}. Run \`trstlyr help\` for usage.`);
272288
process.exit(1);
273289
}
290+
} catch (err) {
291+
console.error(c.red(`Fatal error: ${err instanceof Error ? err.message : String(err)}`));
292+
process.exit(1);
293+
}
274294
}

packages/core/src/__tests__/engine.test.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -177,23 +177,20 @@ describe('TrustEngine — provider failure handling', () => {
177177
expect(result).toBeTruthy();
178178
});
179179

180-
it('provider timeout: slow provider does not block result indefinitely', async () => {
180+
it('provider timeout: slow provider is recorded as unresolved', async () => {
181181
const slowProvider = makeProvider('github', 0.9, 0.95, { delayMs: 5000 });
182182
const engine = new TrustEngine({
183183
providers: [slowProvider],
184184
scoring: { providerTimeout: 100 }, // 100ms timeout
185185
});
186186

187-
const start = Date.now();
188187
const result = await engine.query({
189188
subject: { type: 'agent', namespace: 'github', id: 'slow' },
190189
});
191-
const elapsed = Date.now() - start;
192190

193-
// Should have timed out well before 5s
194-
expect(elapsed).toBeLessThan(2000);
195191
// Unresolved entry recorded for the timeout
196192
expect(result.unresolved.length).toBeGreaterThan(0);
193+
expect(result.unresolved[0]?.reason).toMatch(/timeout/i);
197194
}, 10_000);
198195

199196
it('all providers fail → score 0, critical, all providers in unresolved', async () => {
@@ -268,22 +265,16 @@ describe('TrustEngine — introspection', () => {
268265
// ── Score sanity across provider combinations ─────────────────────────────────
269266

270267
describe('TrustEngine — scoring sanity', () => {
271-
it('two strong agreeing signals → higher score than one alone', async () => {
268+
it('trust_score is bounded [0,100] for single and multi-provider engines', async () => {
272269
const single = makeEngine([makeProvider('github', 0.85, 0.9)]);
273270
const double = makeEngine([
274271
makeProvider('github', 0.85, 0.9),
275272
makeProvider('twitter', 0.80, 0.85),
276273
]);
277274

278275
const r1 = await single.query({ subject: { type: 'agent', namespace: 'github', id: 'alice' } });
279-
// Double engine needs to handle both namespaces — query github (supported by both namespaces provider)
280-
// Use a subject that both providers support by using namespace 'github' for first, twitter for second
281-
// Actually: query github:alice — only github provider fires in double too (twitter doesn't support github ns)
282-
// So let's test by querying a subject where both match: use a neutral subject key
283-
// Better: just verify the double-provider result has more signals and lower uncertainty
284276
const r2 = await double.query({ subject: { type: 'agent', namespace: 'github', id: 'alice2' } });
285-
// Both results should be valid; single vs double isn't directly comparable without same subject
286-
// Core invariant: trust_score is in [0,100]
277+
287278
expect(r1.trust_score).toBeGreaterThanOrEqual(0);
288279
expect(r1.trust_score).toBeLessThanOrEqual(100);
289280
expect(r2.trust_score).toBeGreaterThanOrEqual(0);

packages/core/src/engine/trust-engine.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { TTL, HTTP, FRAUD } from '../constants.js';
99
// });
1010

1111
import type {
12-
AegisConfig,
12+
TrstLyrConfig,
1313
EvaluateRequest,
1414
FraudSignal,
1515
Provider,
@@ -49,7 +49,7 @@ export class TrustEngine {
4949
/** In-flight queries — deduplicates simultaneous requests for the same subject */
5050
private readonly inFlight = new Map<string, Promise<TrustResult>>();
5151

52-
constructor(config: AegisConfig = {}) {
52+
constructor(config: TrstLyrConfig = {}) {
5353
// Build default provider set based on available env vars
5454
// Explicit config.providers always wins; otherwise auto-detect from env
5555
this.providers =
@@ -300,7 +300,7 @@ export class TrustEngine {
300300
};
301301
} catch (err) {
302302
// Attestation failure is non-fatal — log and continue
303-
console.warn('[aegis] EAS attestation failed:', err instanceof Error ? err.message : err);
303+
console.warn('[trstlyr] EAS attestation failed:', err instanceof Error ? err.message : err);
304304
}
305305
}
306306

0 commit comments

Comments
 (0)