Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 2 additions & 15 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from "react";
import type { RiskAssessment } from "@custos/core";
import { requiresFriction, requiresHardBlock } from "@custos/core";
import { analyzeSendIntent } from "./compositionRoot.js";
import { RiskResult } from "./RiskResult.js";

export function App() {
const [address, setAddress] = useState("");
Expand Down Expand Up @@ -62,20 +62,7 @@ export function App() {
{busy ? "Analyzing on-device..." : "Check before sending"}
</button>

{assessment && (
<div
style={{
marginTop: 20,
padding: 16,
border: "2px solid",
borderColor: requiresHardBlock(assessment.level) ? "#c0392b" : requiresFriction(assessment.level) ? "#e67e22" : "#27ae60",
}}
>
<strong>Risk level: {assessment.level}</strong>
<p>{assessment.summary}</p>
{requiresHardBlock(assessment.level) && <p>This send would be blocked — WDK signing must not proceed.</p>}
</div>
)}
{assessment && <RiskResult assessment={assessment} />}
</main>
);
}
27 changes: 27 additions & 0 deletions apps/web/src/RiskResult.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { RiskAssessment } from "@custos/core";
import { requiresHardBlock } from "@custos/core";
import { describeRiskLevel, describeScamCategory } from "./riskPresentation.js";

export function RiskResult({ assessment }: { assessment: RiskAssessment }) {
const { label, color, icon } = describeRiskLevel(assessment.level);

return (
<div style={{ marginTop: 20, padding: 16, border: "2px solid", borderColor: color }}>
<strong style={{ color }}>
{icon} {label}
</strong>
<p>{assessment.summary}</p>
{assessment.matches.length > 0 && (
<ul style={{ paddingLeft: 20 }}>
{assessment.matches.map((match, i) => (
<li key={i}>
{describeScamCategory(match.pattern.category)}
{match.evidenceSnippet ? ` — ${match.evidenceSnippet}` : ""}
</li>
))}
</ul>
)}
{requiresHardBlock(assessment.level) && <p>This send would be blocked — WDK signing must not proceed.</p>}
</div>
);
}
39 changes: 39 additions & 0 deletions apps/web/src/riskPresentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { RiskLevel } from "@custos/core";
import type { ScamCategory } from "@custos/core";

export interface RiskLevelPresentation {
readonly label: string;
readonly color: string;
readonly icon: string;
}

const RISK_LEVEL_PRESENTATION: Record<RiskLevel, RiskLevelPresentation> = {
[RiskLevel.None]: { label: "No known risk", color: "#27ae60", icon: "✅" },
[RiskLevel.Low]: { label: "Low — minor signals detected", color: "#16a085", icon: "ℹ️" },
[RiskLevel.Elevated]: { label: "Elevated — review before sending", color: "#e67e22", icon: "⚠️" },
[RiskLevel.High]: { label: "High — strong scam signals", color: "#d35400", icon: "🔶" },
[RiskLevel.Critical]: { label: "Critical — send blocked", color: "#c0392b", icon: "🛑" },
};

export function describeRiskLevel(level: RiskLevel): RiskLevelPresentation {
return RISK_LEVEL_PRESENTATION[level];
}

const SCAM_CATEGORY_LABEL: Record<ScamCategory, string> = {
"pig-butchering": "Pig-butchering scam",
"fake-support": "Fake support impersonation",
"recovery-scam": "Fund recovery scam",
"romance-scam": "Romance scam",
"fake-investment-platform": "Fake investment platform",
"urgency-pressure": "Urgency/pressure tactics",
"address-poisoning": "Lookalike address (poisoning attempt)",
"task-scam": "Task/job scam",
"impersonation-authority": "Authority impersonation",
"airdrop-claim-fee": "Fake airdrop claim fee",
"otc-escrow-scam": "OTC escrow scam",
"phishing-approval-drain": "Phishing / approval drain",
};

export function describeScamCategory(category: ScamCategory): string {
return SCAM_CATEGORY_LABEL[category];
}
Loading