Vedant Sanjay Jadhav commited on
Commit
e75eaba
·
1 Parent(s): 3d7223c

feat: implement Vercel Next.js server-side API proxy architecture to eliminate browser CORS

Browse files
frontend/.env.example CHANGED
@@ -1,2 +1,3 @@
1
- # RazorShield Frontend Environment Variables
 
2
  NEXT_PUBLIC_API_URL=https://vedantjadhav701-razorshield-api.hf.space
 
1
+ # RazorShield Frontend Environment Variables (Server-Only Backend URL)
2
+ RAZORSHIELD_BACKEND_URL=https://vedantjadhav701-razorshield-api.hf.space
3
  NEXT_PUBLIC_API_URL=https://vedantjadhav701-razorshield-api.hf.space
frontend/app/api/explain/route.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { explainEvidence } from "@/lib/server/razorshield";
3
+
4
+ export async function POST(req: NextRequest) {
5
+ try {
6
+ const body = await req.json();
7
+ const evidenceJson = typeof body.evidence_json === "string" ? body.evidence_json : JSON.stringify(body);
8
+ const result = await explainEvidence(evidenceJson);
9
+ if (!result.success) {
10
+ return NextResponse.json(result, { status: 500 });
11
+ }
12
+ return NextResponse.json(result, { status: 200 });
13
+ } catch (err: any) {
14
+ return NextResponse.json(
15
+ {
16
+ success: false,
17
+ error: {
18
+ code: "INVALID_REQUEST",
19
+ message: "Failed to parse JSON body for explanation.",
20
+ details: err.message,
21
+ },
22
+ },
23
+ { status: 400 }
24
+ );
25
+ }
26
+ }
frontend/app/api/health/route.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from "next/server";
2
+ import { healthCheck } from "@/lib/server/razorshield";
3
+
4
+ export async function GET() {
5
+ const result = await healthCheck();
6
+ if (!result.success) {
7
+ return NextResponse.json(result, { status: 503 });
8
+ }
9
+ return NextResponse.json(result, { status: 200 });
10
+ }
frontend/app/api/merchant/route.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { analyzeMerchant } from "@/lib/server/razorshield";
3
+
4
+ export async function POST(req: NextRequest) {
5
+ try {
6
+ const body = await req.json();
7
+ const merchantId = body.merchant_id || "M_101";
8
+ const result = await analyzeMerchant(merchantId);
9
+ if (!result.success) {
10
+ return NextResponse.json(result, { status: 500 });
11
+ }
12
+ return NextResponse.json(result, { status: 200 });
13
+ } catch (err: any) {
14
+ return NextResponse.json(
15
+ {
16
+ success: false,
17
+ error: {
18
+ code: "INVALID_REQUEST",
19
+ message: "Failed to parse JSON request body.",
20
+ details: err.message,
21
+ },
22
+ },
23
+ { status: 400 }
24
+ );
25
+ }
26
+ }
frontend/app/api/reset/route.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from "next/server";
2
+ import { resetDemoState } from "@/lib/server/razorshield";
3
+
4
+ export async function POST() {
5
+ const result = await resetDemoState();
6
+ if (!result.success) {
7
+ return NextResponse.json(result, { status: 500 });
8
+ }
9
+ return NextResponse.json(result, { status: 200 });
10
+ }
frontend/app/api/scenario/route.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { runScenario } from "@/lib/server/razorshield";
3
+
4
+ export async function POST(req: NextRequest) {
5
+ try {
6
+ const body = await req.json();
7
+ const scenarioName = body.scenario_name || "FRAUD_SPIKE";
8
+ const policyMode = body.policy_mode || "BALANCED";
9
+ const result = await runScenario(scenarioName, policyMode);
10
+ if (!result.success) {
11
+ return NextResponse.json(result, { status: 500 });
12
+ }
13
+ return NextResponse.json(result, { status: 200 });
14
+ } catch (err: any) {
15
+ return NextResponse.json(
16
+ {
17
+ success: false,
18
+ error: {
19
+ code: "INVALID_REQUEST",
20
+ message: "Failed to parse JSON body for scenario replay.",
21
+ details: err.message,
22
+ },
23
+ },
24
+ { status: 400 }
25
+ );
26
+ }
27
+ }
frontend/app/api/transaction/route.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { analyzeTransaction } from "@/lib/server/razorshield";
3
+
4
+ export async function POST(req: NextRequest) {
5
+ try {
6
+ const body = await req.json();
7
+ const result = await analyzeTransaction(body);
8
+ if (!result.success) {
9
+ return NextResponse.json(result, { status: 500 });
10
+ }
11
+ return NextResponse.json(result, { status: 200 });
12
+ } catch (err: any) {
13
+ return NextResponse.json(
14
+ {
15
+ success: false,
16
+ error: {
17
+ code: "INVALID_REQUEST",
18
+ message: "Failed to parse JSON body or invalid transaction payload.",
19
+ details: err.message,
20
+ },
21
+ },
22
+ { status: 400 }
23
+ );
24
+ }
25
+ }
frontend/lib/api.ts CHANGED
@@ -1,10 +1,10 @@
1
  /**
2
  * api.ts
3
- * Hugging Face Gradio Space API Integration Layer.
4
- * Communicates with backend Space: vedantjadhav701/razorshield-api
 
5
  */
6
 
7
- import { Client } from "@gradio/client";
8
  import {
9
  AnalyzeTransactionResponse,
10
  BackendHealthStatus,
@@ -14,80 +14,42 @@ import {
14
  TransactionApiInput,
15
  } from "./types";
16
 
17
- const SPACE_NAME = "vedantjadhav701/razorshield-api";
18
- const DEFAULT_URL = process.env.NEXT_PUBLIC_API_URL || `https://${SPACE_NAME.replace("/", "-")}.hf.space`;
19
-
20
- let gradioClient: Client | null = null;
21
-
22
  function generateRequestId(): string {
23
  return `REQ_${Math.random().toString(36).substring(2, 8).toUpperCase()}`;
24
  }
25
 
26
- async function getClient(): Promise<Client | null> {
27
- if (gradioClient) return gradioClient;
28
- try {
29
- gradioClient = await Client.connect(SPACE_NAME);
30
- return gradioClient;
31
- } catch (err) {
32
- console.warn("Gradio Client connection warning. Falling back to direct HTTP fetch:", err);
33
- return null;
34
- }
35
- }
36
-
37
  /**
38
- * Real Backend Health Check Handshake
39
  */
40
  export async function checkBackendHealth(): Promise<BackendHealthStatus> {
41
  const start = performance.now();
42
  const sentAt = new Date().toISOString();
43
  try {
44
- const client = await getClient();
45
- if (client) {
46
- // Test prediction on analyze_merchant
47
- await client.predict("analyze_merchant", ["M_HEALTH_CHECK"]);
48
- const latency = Math.round(performance.now() - start);
49
- return {
50
- status: "CONNECTED",
51
- endpoint: DEFAULT_URL,
52
- last_sync_at: new Date().toISOString(),
53
- roundtrip_latency_ms: latency,
54
- };
55
- }
56
- } catch (err) {
57
- console.warn("Gradio health check error:", err);
58
- }
59
-
60
- // HTTP endpoint fallback check
61
- try {
62
- const res = await fetch(`${DEFAULT_URL}/api/predict/analyze_merchant`, {
63
- method: "POST",
64
- headers: { "Content-Type": "application/json" },
65
- body: JSON.stringify({ data: ["M_HEALTH_CHECK"] }),
66
- });
67
  const latency = Math.round(performance.now() - start);
68
  if (res.ok) {
69
  return {
70
  status: "CONNECTED",
71
- endpoint: DEFAULT_URL,
72
  last_sync_at: new Date().toISOString(),
73
  roundtrip_latency_ms: latency,
74
  };
75
  }
76
- } catch (err) {
77
- console.error("HTTP health check failed:", err);
78
  }
79
 
80
  return {
81
  status: "OFFLINE",
82
- endpoint: DEFAULT_URL,
83
  last_sync_at: sentAt,
84
  roundtrip_latency_ms: null,
85
- error: "Hugging Face Space unreachable",
86
  };
87
  }
88
 
89
  /**
90
- * Analyzes a single transaction payload using the deployed Hugging Face Space backend.
91
  */
92
  export async function analyzeTransaction(
93
  input: TransactionApiInput
@@ -96,121 +58,33 @@ export async function analyzeTransaction(
96
  const sentAt = new Date().toISOString();
97
  const start = performance.now();
98
 
99
- const mId = input.merchant_id || "M_101";
100
- const txId = input.transaction_id || `TX_${Date.now().toString().slice(-6)}`;
101
- const custId = input.customer_id || "C_1048";
102
- const devId = input.device_id || "D_882";
103
- const eventTime = input.event_time || new Date().toISOString();
104
- const amount = input.amount || 100.0;
105
- const pm = input.payment_method || "card";
106
- const tt = input.transaction_type || "sale";
107
- const policyMode = input.policy_mode || "BALANCED";
108
-
109
- const client = await getClient();
110
 
111
- if (client) {
112
- try {
113
- const res = await client.predict("analyze_transaction", [
114
- mId,
115
- txId,
116
- custId,
117
- devId,
118
- eventTime,
119
- amount,
120
- pm,
121
- tt,
122
- policyMode,
123
- ]);
124
- const roundtrip = Math.round(performance.now() - start);
125
- const dataStr = Array.isArray(res.data) ? (res.data[0] as string) : String(res.data);
126
- const parsed: AnalyzeTransactionResponse = JSON.parse(dataStr);
127
- parsed.meta = {
128
- request_id: reqId,
129
- request_sent_at: sentAt,
130
- response_received_at: new Date().toISOString(),
131
- roundtrip_latency_ms: roundtrip,
132
- data_source: "LIVE HUGGING FACE BACKEND",
133
- };
134
- return parsed;
135
- } catch (err) {
136
- console.error("Gradio predict error for analyze_transaction:", err);
137
- }
138
- }
139
 
140
- // HTTP Direct Fallback
141
- try {
142
- const httpRes = await fetch(`${DEFAULT_URL}/api/predict/analyze_transaction`, {
143
- method: "POST",
144
- headers: { "Content-Type": "application/json" },
145
- body: JSON.stringify({
146
- data: [mId, txId, custId, devId, eventTime, amount, pm, tt, policyMode],
147
- }),
148
- });
149
- const roundtrip = Math.round(performance.now() - start);
150
- if (httpRes.ok) {
151
- const json = await httpRes.json();
152
- const rawText = Array.isArray(json.data) ? json.data[0] : json.data;
153
- const parsed: AnalyzeTransactionResponse = JSON.parse(rawText);
154
- parsed.meta = {
155
- request_id: reqId,
156
- request_sent_at: sentAt,
157
- response_received_at: new Date().toISOString(),
158
- roundtrip_latency_ms: roundtrip,
159
- data_source: "LIVE HUGGING FACE BACKEND",
160
- };
161
- return parsed;
162
- }
163
- } catch (httpErr) {
164
- console.warn("Direct HTTP endpoint unavailable:", httpErr);
165
  }
166
 
167
- // Deterministic Client Fallback
168
- const roundtrip = Math.round(performance.now() - start);
169
- return {
170
- transaction_id: txId,
171
- merchant_id: mId,
172
- transaction_risk: { fraud_probability: 0.012 },
173
- merchant_risk: {
174
- spike_probability: 0.04,
175
- fraud_excess_ratio: 1.0,
176
- velocity_ratio: 1.0,
177
- incident_state: "NORMAL",
178
- severity: "LOW",
179
- incident_score: 0.06,
180
- suspicious_windows: 0,
181
- },
182
- campaign: { active: false, campaign_name: null },
183
- decision: { action: "APPROVE", policy_mode: policyMode },
184
- explanation: {
185
- title: "RazorShield Defensive Risk Assessment: NORMAL (LOW Severity)",
186
- summary: `RazorShield evaluated merchant ${mId} activity as NORMAL (LOW severity). Observed fraud excess ratio is 1.0x baseline and volume velocity is 1.0x baseline.`,
187
- key_signals: [
188
- "Policy Incident Score: 0.06",
189
- "Fraud Excess Ratio: 1.0x baseline",
190
- "Volume Velocity Ratio: 1.0x baseline",
191
- "Consecutive Suspicious Windows: 0",
192
- ],
193
- campaign_context: `No promotional campaign is active for merchant ${mId}.`,
194
- recommended_action: "Maintain standard automated processing.",
195
- confidence_note: "Explanation generated via deterministic fallback.",
196
- },
197
- performance: {
198
- risk_engine_latency_ms: 0.62,
199
- slm_latency_ms: 0.0,
200
- total_latency_ms: 0.62,
201
- },
202
- meta: {
203
- request_id: reqId,
204
- request_sent_at: sentAt,
205
- response_received_at: new Date().toISOString(),
206
- roundtrip_latency_ms: roundtrip,
207
- data_source: "CLIENT FALLBACK",
208
- },
209
  };
 
 
210
  }
211
 
212
  /**
213
- * Replays a test scenario through the Hugging Face Space backend.
214
  */
215
  export async function runScenarioReplay(
216
  scenarioName: string,
@@ -220,164 +94,109 @@ export async function runScenarioReplay(
220
  const sentAt = new Date().toISOString();
221
  const start = performance.now();
222
 
223
- const client = await getClient();
 
 
 
 
224
 
225
- if (client) {
226
- try {
227
- const res = await client.predict("run_scenario", [scenarioName, policyMode]);
228
- const roundtrip = Math.round(performance.now() - start);
229
- const dataStr = Array.isArray(res.data) ? (res.data[0] as string) : String(res.data);
230
- const parsed: ScenarioReplayResult = JSON.parse(dataStr);
231
- parsed.meta = {
232
- request_id: reqId,
233
- request_sent_at: sentAt,
234
- response_received_at: new Date().toISOString(),
235
- roundtrip_latency_ms: roundtrip,
236
- data_source: "LIVE HUGGING FACE BACKEND",
237
- };
238
- return parsed;
239
- } catch (err) {
240
- console.error("Gradio predict error for run_scenario:", err);
241
- }
242
  }
243
 
244
- // Deterministic Fallback Replay Data
245
- const roundtrip = Math.round(performance.now() - start);
246
- const isAlert = scenarioName.includes("FRAUD");
247
- return {
248
- scenario_name: scenarioName,
249
- scenario_id: `sc_demo_${Date.now()}`,
250
- merchant_id: `M_DEMO_${scenarioName}`,
251
- total_transactions: 600,
252
- replay_time_ms: 112.5,
253
- incident_state_distribution: {
254
- NORMAL: isAlert ? 570 : 600,
255
- INVESTIGATE: 0,
256
- ALERT: isAlert ? 30 : 0,
257
- },
258
- final_incident_state: isAlert ? "ALERT" : "NORMAL",
259
- final_severity: isAlert ? "HIGH" : "LOW",
260
- explanation: {
261
- title: `RazorShield Defensive Risk Assessment: ${isAlert ? "ALERT" : "NORMAL"}`,
262
- summary: isAlert
263
- ? "RazorShield classified merchant activity as ALERT due to persistent fraud excess ratio surging to 2.5x baseline across monitoring windows."
264
- : "RazorShield classified merchant activity as NORMAL. Volume surge is normalized with fraud excess ratio remaining 1.0x baseline.",
265
- key_signals: [
266
- `Incident State: ${isAlert ? "ALERT" : "NORMAL"}`,
267
- `Fraud Excess Ratio: ${isAlert ? "2.5x" : "1.0x"}`,
268
- ],
269
- campaign_context: scenarioName.includes("FLASH")
270
- ? "Promotional campaign registered. Volume velocity is normalized."
271
- : "No campaign active.",
272
- recommended_action: isAlert ? "Initiate merchant review & verification." : "Maintain standard processing.",
273
- confidence_note: "Deterministic scenario replay result.",
274
- },
275
- meta: {
276
- request_id: reqId,
277
- request_sent_at: sentAt,
278
- response_received_at: new Date().toISOString(),
279
- roundtrip_latency_ms: roundtrip,
280
- data_source: "CLIENT FALLBACK",
281
- },
282
  };
 
 
283
  }
284
 
285
  /**
286
- * Queries live merchant temporal state.
287
  */
288
  export async function queryMerchantState(merchantId: string): Promise<MerchantStateQueryResponse> {
289
  const reqId = generateRequestId();
290
  const sentAt = new Date().toISOString();
291
  const start = performance.now();
292
 
293
- const client = await getClient();
 
 
 
 
294
 
295
- if (client) {
296
- try {
297
- const res = await client.predict("analyze_merchant", [merchantId]);
298
- const roundtrip = Math.round(performance.now() - start);
299
- const dataStr = Array.isArray(res.data) ? (res.data[0] as string) : String(res.data);
300
- const parsed: MerchantStateQueryResponse = JSON.parse(dataStr);
301
- parsed.meta = {
302
- request_id: reqId,
303
- request_sent_at: sentAt,
304
- response_received_at: new Date().toISOString(),
305
- roundtrip_latency_ms: roundtrip,
306
- data_source: "LIVE HUGGING FACE BACKEND",
307
- };
308
- return parsed;
309
- } catch (err) {
310
- console.error("Gradio predict error for analyze_merchant:", err);
311
- }
312
  }
313
 
314
- const roundtrip = Math.round(performance.now() - start);
315
- return {
316
- merchant_id: merchantId,
317
- rolling_window: {
318
- rolling_txn_count_15m: 15,
319
- baseline_txn_count_15m: 15,
320
- velocity_ratio: 1.0,
321
- estimated_fraud_count: 0.12,
322
- expected_fraud_count: 0.12,
323
- fraud_excess_ratio: 1.0,
324
- },
325
- incident_state: {
326
- merchant_id: merchantId,
327
- current_spike_probability: 0.05,
328
- current_fraud_excess_ratio: 1.0,
329
- current_velocity_ratio: 1.0,
330
- suspicious_transaction_count: 0,
331
- consecutive_suspicious_windows: 0,
332
- campaign_active: false,
333
- },
334
- meta: {
335
- request_id: reqId,
336
- request_sent_at: sentAt,
337
- response_received_at: new Date().toISOString(),
338
- roundtrip_latency_ms: roundtrip,
339
- data_source: "CLIENT FALLBACK",
340
- },
341
  };
 
 
342
  }
343
 
344
  /**
345
- * Resets all demo merchant states.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  */
347
  export async function resetDemoState(): Promise<{ status: string; message: string; meta?: ResponseMetadata }> {
348
  const reqId = generateRequestId();
349
  const sentAt = new Date().toISOString();
350
  const start = performance.now();
351
 
352
- const client = await getClient();
353
- if (client) {
354
- try {
355
- const res = await client.predict("reset_demo_state", []);
356
- const roundtrip = Math.round(performance.now() - start);
357
- const dataStr = Array.isArray(res.data) ? (res.data[0] as string) : String(res.data);
358
- const parsed = JSON.parse(dataStr);
359
- parsed.meta = {
360
- request_id: reqId,
361
- request_sent_at: sentAt,
362
- response_received_at: new Date().toISOString(),
363
- roundtrip_latency_ms: roundtrip,
364
- data_source: "LIVE HUGGING FACE BACKEND",
365
- };
366
- return parsed;
367
- } catch (err) {
368
- console.error("Gradio predict error for reset_demo_state:", err);
369
- }
370
- }
371
  const roundtrip = Math.round(performance.now() - start);
372
- return {
373
- status: "SUCCESS",
374
- message: "Demo state reset completed.",
375
- meta: {
376
- request_id: reqId,
377
- request_sent_at: sentAt,
378
- response_received_at: new Date().toISOString(),
379
- roundtrip_latency_ms: roundtrip,
380
- data_source: "CLIENT FALLBACK",
381
- },
 
 
 
382
  };
 
 
383
  }
 
1
  /**
2
  * api.ts
3
+ * Browser-Side API Client.
4
+ * Communicates ONLY with same-origin Next.js Server Route Handlers (/api/...).
5
+ * The browser never connects directly to Hugging Face Spaces.
6
  */
7
 
 
8
  import {
9
  AnalyzeTransactionResponse,
10
  BackendHealthStatus,
 
14
  TransactionApiInput,
15
  } from "./types";
16
 
 
 
 
 
 
17
  function generateRequestId(): string {
18
  return `REQ_${Math.random().toString(36).substring(2, 8).toUpperCase()}`;
19
  }
20
 
 
 
 
 
 
 
 
 
 
 
 
21
  /**
22
+ * Health check probe via /api/health
23
  */
24
  export async function checkBackendHealth(): Promise<BackendHealthStatus> {
25
  const start = performance.now();
26
  const sentAt = new Date().toISOString();
27
  try {
28
+ const res = await fetch("/api/health", { cache: "no-store" });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  const latency = Math.round(performance.now() - start);
30
  if (res.ok) {
31
  return {
32
  status: "CONNECTED",
33
+ endpoint: "/api/health (Vercel Server Proxy)",
34
  last_sync_at: new Date().toISOString(),
35
  roundtrip_latency_ms: latency,
36
  };
37
  }
38
+ } catch (err: any) {
39
+ console.warn("Health check error via /api/health:", err);
40
  }
41
 
42
  return {
43
  status: "OFFLINE",
44
+ endpoint: "/api/health",
45
  last_sync_at: sentAt,
46
  roundtrip_latency_ms: null,
47
+ error: "Server-side RazorShield proxy unreachable",
48
  };
49
  }
50
 
51
  /**
52
+ * Analyzes transaction via POST /api/transaction
53
  */
54
  export async function analyzeTransaction(
55
  input: TransactionApiInput
 
58
  const sentAt = new Date().toISOString();
59
  const start = performance.now();
60
 
61
+ const res = await fetch("/api/transaction", {
62
+ method: "POST",
63
+ headers: { "Content-Type": "application/json" },
64
+ body: JSON.stringify(input),
65
+ });
 
 
 
 
 
 
66
 
67
+ const roundtrip = Math.round(performance.now() - start);
68
+ const json = await res.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
+ if (!res.ok || !json.success || !json.data) {
71
+ throw new Error(json?.error?.message || `Transaction analysis failed with HTTP ${res.status}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  }
73
 
74
+ const parsed: AnalyzeTransactionResponse = json.data;
75
+ parsed.meta = {
76
+ request_id: reqId,
77
+ request_sent_at: sentAt,
78
+ response_received_at: new Date().toISOString(),
79
+ roundtrip_latency_ms: roundtrip,
80
+ data_source: "LIVE HUGGING FACE BACKEND",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  };
82
+
83
+ return parsed;
84
  }
85
 
86
  /**
87
+ * Replays scenario via POST /api/scenario
88
  */
89
  export async function runScenarioReplay(
90
  scenarioName: string,
 
94
  const sentAt = new Date().toISOString();
95
  const start = performance.now();
96
 
97
+ const res = await fetch("/api/scenario", {
98
+ method: "POST",
99
+ headers: { "Content-Type": "application/json" },
100
+ body: JSON.stringify({ scenario_name: scenarioName, policy_mode: policyMode }),
101
+ });
102
 
103
+ const roundtrip = Math.round(performance.now() - start);
104
+ const json = await res.json();
105
+
106
+ if (!res.ok || !json.success || !json.data) {
107
+ throw new Error(json?.error?.message || `Scenario replay failed with HTTP ${res.status}`);
 
 
 
 
 
 
 
 
 
 
 
 
108
  }
109
 
110
+ const parsed: ScenarioReplayResult = json.data;
111
+ parsed.meta = {
112
+ request_id: reqId,
113
+ request_sent_at: sentAt,
114
+ response_received_at: new Date().toISOString(),
115
+ roundtrip_latency_ms: roundtrip,
116
+ data_source: "LIVE HUGGING FACE BACKEND",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  };
118
+
119
+ return parsed;
120
  }
121
 
122
  /**
123
+ * Queries merchant state via POST /api/merchant
124
  */
125
  export async function queryMerchantState(merchantId: string): Promise<MerchantStateQueryResponse> {
126
  const reqId = generateRequestId();
127
  const sentAt = new Date().toISOString();
128
  const start = performance.now();
129
 
130
+ const res = await fetch("/api/merchant", {
131
+ method: "POST",
132
+ headers: { "Content-Type": "application/json" },
133
+ body: JSON.stringify({ merchant_id: merchantId }),
134
+ });
135
 
136
+ const roundtrip = Math.round(performance.now() - start);
137
+ const json = await res.json();
138
+
139
+ if (!res.ok || !json.success || !json.data) {
140
+ throw new Error(json?.error?.message || `Merchant query failed with HTTP ${res.status}`);
 
 
 
 
 
 
 
 
 
 
 
 
141
  }
142
 
143
+ const parsed: MerchantStateQueryResponse = json.data;
144
+ parsed.meta = {
145
+ request_id: reqId,
146
+ request_sent_at: sentAt,
147
+ response_received_at: new Date().toISOString(),
148
+ roundtrip_latency_ms: roundtrip,
149
+ data_source: "LIVE HUGGING FACE BACKEND",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  };
151
+
152
+ return parsed;
153
  }
154
 
155
  /**
156
+ * Explains evidence via POST /api/explain
157
+ */
158
+ export async function explainEvidencePayload(evidenceJson: string): Promise<any> {
159
+ const res = await fetch("/api/explain", {
160
+ method: "POST",
161
+ headers: { "Content-Type": "application/json" },
162
+ body: JSON.stringify({ evidence_json: evidenceJson }),
163
+ });
164
+
165
+ const json = await res.json();
166
+ if (!res.ok || !json.success) {
167
+ throw new Error(json?.error?.message || `Explanation failed with HTTP ${res.status}`);
168
+ }
169
+
170
+ return json.data;
171
+ }
172
+
173
+ /**
174
+ * Resets demo state via POST /api/reset
175
  */
176
  export async function resetDemoState(): Promise<{ status: string; message: string; meta?: ResponseMetadata }> {
177
  const reqId = generateRequestId();
178
  const sentAt = new Date().toISOString();
179
  const start = performance.now();
180
 
181
+ const res = await fetch("/api/reset", {
182
+ method: "POST",
183
+ });
184
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  const roundtrip = Math.round(performance.now() - start);
186
+ const json = await res.json();
187
+
188
+ if (!res.ok || !json.success || !json.data) {
189
+ throw new Error(json?.error?.message || `Reset demo state failed with HTTP ${res.status}`);
190
+ }
191
+
192
+ const parsed = json.data;
193
+ parsed.meta = {
194
+ request_id: reqId,
195
+ request_sent_at: sentAt,
196
+ response_received_at: new Date().toISOString(),
197
+ roundtrip_latency_ms: roundtrip,
198
+ data_source: "LIVE HUGGING FACE BACKEND",
199
  };
200
+
201
+ return parsed;
202
  }
frontend/lib/server/razorshield.ts ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * razorshield.ts
3
+ * Server-Side RazorShield API Client.
4
+ * Runs exclusively on the Vercel Node.js Server.
5
+ * Communicates server-to-server with Hugging Face Space backend (Gradio 5+ API).
6
+ */
7
+
8
+ import {
9
+ AnalyzeTransactionResponse,
10
+ MerchantStateQueryResponse,
11
+ ScenarioReplayResult,
12
+ TransactionApiInput,
13
+ } from "../types";
14
+
15
+ const BACKEND_URL =
16
+ process.env.RAZORSHIELD_BACKEND_URL ||
17
+ process.env.NEXT_PUBLIC_API_URL ||
18
+ "https://vedantjadhav701-razorshield-api.hf.space";
19
+
20
+ export interface ApiResponse<T> {
21
+ success: boolean;
22
+ data?: T;
23
+ error?: {
24
+ code: string;
25
+ message: string;
26
+ details?: string;
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Low-level server-to-server execution helper calling Gradio 5+ /gradio_api/call endpoints.
32
+ */
33
+ async function callGradioApi<T = any>(apiName: string, data: any[]): Promise<T> {
34
+ const cleanUrl = BACKEND_URL.replace(/\/$/, "");
35
+ const postUrl = `${cleanUrl}/gradio_api/call/${apiName}`;
36
+
37
+ // Step 1: Initiate execution
38
+ const postRes = await fetch(postUrl, {
39
+ method: "POST",
40
+ headers: { "Content-Type": "application/json" },
41
+ body: JSON.stringify({ data }),
42
+ cache: "no-store",
43
+ });
44
+
45
+ if (!postRes.ok) {
46
+ throw new Error(`Gradio POST to ${apiName} failed with HTTP ${postRes.status}`);
47
+ }
48
+
49
+ const { event_id } = await postRes.json();
50
+ if (!event_id) {
51
+ throw new Error(`Gradio POST to ${apiName} did not return an event_id`);
52
+ }
53
+
54
+ // Step 2: Retrieve stream result
55
+ const getUrl = `${cleanUrl}/gradio_api/call/${apiName}/${event_id}`;
56
+ const getRes = await fetch(getUrl, { cache: "no-store" });
57
+
58
+ if (!getRes.ok) {
59
+ throw new Error(`Gradio GET stream for ${apiName} failed with HTTP ${getRes.status}`);
60
+ }
61
+
62
+ const text = await getRes.text();
63
+ const lines = text.split("\n");
64
+ for (const line of lines) {
65
+ if (line.startsWith("data: ")) {
66
+ const rawDataStr = line.substring(6).trim();
67
+ const parsedArray = JSON.parse(rawDataStr);
68
+ const rawResult = Array.isArray(parsedArray) ? parsedArray[0] : parsedArray;
69
+ if (typeof rawResult === "string") {
70
+ try {
71
+ return JSON.parse(rawResult) as T;
72
+ } catch {
73
+ return rawResult as unknown as T;
74
+ }
75
+ }
76
+ return rawResult as T;
77
+ }
78
+ }
79
+
80
+ throw new Error(`No complete data stream received for Gradio API ${apiName}`);
81
+ }
82
+
83
+ /**
84
+ * Server-side Health Check
85
+ */
86
+ export async function healthCheck(): Promise<ApiResponse<{ status: string; service: string; model: string; slm_loaded: boolean; policy_mode: string }>> {
87
+ try {
88
+ const start = performance.now();
89
+ await callGradioApi("analyze_merchant", ["M_HEALTH_CHECK"]);
90
+ const latency = Math.round(performance.now() - start);
91
+
92
+ return {
93
+ success: true,
94
+ data: {
95
+ status: "ok",
96
+ service: "RazorShield Risk Intelligence",
97
+ model: "Qwen/Qwen2.5-0.5B-Instruct",
98
+ slm_loaded: true,
99
+ policy_mode: "BALANCED",
100
+ },
101
+ };
102
+ } catch (err: any) {
103
+ console.error("Server-side healthCheck failed:", err.message || err);
104
+ return {
105
+ success: false,
106
+ error: {
107
+ code: "BACKEND_UNREACHABLE",
108
+ message: "Hugging Face Space backend is unreachable or offline.",
109
+ details: err.message,
110
+ },
111
+ };
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Server-side Transaction Analysis
117
+ */
118
+ export async function analyzeTransaction(
119
+ input: TransactionApiInput
120
+ ): Promise<ApiResponse<AnalyzeTransactionResponse>> {
121
+ try {
122
+ const mId = input.merchant_id || "M_101";
123
+ const txId = input.transaction_id || `TX_${Date.now().toString().slice(-6)}`;
124
+ const custId = input.customer_id || "C_1048";
125
+ const devId = input.device_id || "D_882";
126
+ const eventTime = input.event_time || new Date().toISOString();
127
+ const amount = input.amount || 100.0;
128
+ const pm = input.payment_method || "card";
129
+ const tt = input.transaction_type || "sale";
130
+ const policyMode = input.policy_mode || "BALANCED";
131
+
132
+ const parsed = await callGradioApi<AnalyzeTransactionResponse>("analyze_transaction", [
133
+ mId,
134
+ txId,
135
+ custId,
136
+ devId,
137
+ eventTime,
138
+ amount,
139
+ pm,
140
+ tt,
141
+ policyMode,
142
+ ]);
143
+
144
+ return {
145
+ success: true,
146
+ data: parsed,
147
+ };
148
+ } catch (err: any) {
149
+ console.error("Server-side analyzeTransaction failed:", err.message || err);
150
+ return {
151
+ success: false,
152
+ error: {
153
+ code: "TRANSACTION_ANALYSIS_FAILED",
154
+ message: "Failed to evaluate transaction through backend risk engine.",
155
+ details: err.message,
156
+ },
157
+ };
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Server-side Merchant State Query
163
+ */
164
+ export async function analyzeMerchant(
165
+ merchantId: string
166
+ ): Promise<ApiResponse<MerchantStateQueryResponse>> {
167
+ try {
168
+ const parsed = await callGradioApi<MerchantStateQueryResponse>("analyze_merchant", [merchantId]);
169
+ return {
170
+ success: true,
171
+ data: parsed,
172
+ };
173
+ } catch (err: any) {
174
+ console.error("Server-side analyzeMerchant failed:", err.message || err);
175
+ return {
176
+ success: false,
177
+ error: {
178
+ code: "MERCHANT_QUERY_FAILED",
179
+ message: "Failed to query merchant incident state from backend.",
180
+ details: err.message,
181
+ },
182
+ };
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Server-side Scenario Replay Execution
188
+ */
189
+ export async function runScenario(
190
+ scenarioName: string,
191
+ policyMode: string = "BALANCED"
192
+ ): Promise<ApiResponse<ScenarioReplayResult>> {
193
+ try {
194
+ const parsed = await callGradioApi<ScenarioReplayResult>("run_scenario", [scenarioName, policyMode]);
195
+ return {
196
+ success: true,
197
+ data: parsed,
198
+ };
199
+ } catch (err: any) {
200
+ console.error("Server-side runScenario failed:", err.message || err);
201
+ return {
202
+ success: false,
203
+ error: {
204
+ code: "SCENARIO_REPLAY_FAILED",
205
+ message: "Failed to execute scenario replay through backend.",
206
+ details: err.message,
207
+ },
208
+ };
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Server-side Evidence Explanation
214
+ */
215
+ export async function explainEvidence(
216
+ evidenceJson: string
217
+ ): Promise<ApiResponse<any>> {
218
+ try {
219
+ const parsed = await callGradioApi("explain_evidence", [evidenceJson]);
220
+ return {
221
+ success: true,
222
+ data: parsed,
223
+ };
224
+ } catch (err: any) {
225
+ console.error("Server-side explainEvidence failed:", err.message || err);
226
+ return {
227
+ success: false,
228
+ error: {
229
+ code: "EXPLANATION_GENERATION_FAILED",
230
+ message: "Failed to generate explanation for evidence.",
231
+ details: err.message,
232
+ },
233
+ };
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Server-side Reset Demo State
239
+ */
240
+ export async function resetDemoState(): Promise<ApiResponse<{ status: string; message: string }>> {
241
+ try {
242
+ const parsed = await callGradioApi<{ status: string; message: string }>("reset_demo_state", []);
243
+ return {
244
+ success: true,
245
+ data: parsed,
246
+ };
247
+ } catch (err: any) {
248
+ console.error("Server-side resetDemoState failed:", err.message || err);
249
+ return {
250
+ success: false,
251
+ error: {
252
+ code: "RESET_FAILED",
253
+ message: "Failed to reset demo state on backend.",
254
+ details: err.message,
255
+ },
256
+ };
257
+ }
258
+ }