@mnemopay/sdk 0.3.0 → 0.4.0

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/dist/fraud.js ADDED
@@ -0,0 +1,633 @@
1
+ "use strict";
2
+ /**
3
+ * MnemoPay Fraud Guard — velocity checks, anomaly detection, risk scoring,
4
+ * dispute resolution, and platform fee collection.
5
+ *
6
+ * Plugs into MnemoPayLite.charge/settle/refund to enforce security rules
7
+ * before any money moves.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.RateLimiter = exports.DEFAULT_RATE_LIMIT = exports.FraudGuard = exports.DEFAULT_FRAUD_CONFIG = void 0;
11
+ const fraud_ml_js_1 = require("./fraud-ml.js");
12
+ exports.DEFAULT_FRAUD_CONFIG = {
13
+ platformFeeRate: 0.03,
14
+ maxChargesPerMinute: 5,
15
+ maxChargesPerHour: 30,
16
+ maxChargesPerDay: 100,
17
+ maxDailyVolume: 5000,
18
+ anomalyStdDevThreshold: 2.5,
19
+ settlementHoldMinutes: 30,
20
+ disputeWindowMinutes: 1440,
21
+ blockThreshold: 0.75,
22
+ flagThreshold: 0.45,
23
+ minAccountAgeMinutes: 0,
24
+ maxPendingTransactions: 10,
25
+ enableGeoCheck: true,
26
+ blockedCountries: [],
27
+ ml: false,
28
+ };
29
+ // ─── Fraud Guard ────────────────────────────────────────────────────────────
30
+ class FraudGuard {
31
+ config;
32
+ /** Sliding window of recent charges per agent */
33
+ chargeHistory = new Map();
34
+ /** Running stats per agent: sum, sumSq, count for online std-dev */
35
+ agentStats = new Map();
36
+ /** Active disputes */
37
+ disputes = new Map();
38
+ /** Platform fee ledger */
39
+ feeLedger = [];
40
+ /** Total platform fees collected */
41
+ _platformFeesCollected = 0;
42
+ /** Known IPs per agent for consistency checks */
43
+ agentIps = new Map();
44
+ /** Flagged agents (soft block — allowed but monitored) */
45
+ flaggedAgents = new Set();
46
+ /** Hard-blocked agents */
47
+ blockedAgents = new Set();
48
+ /** ML anomaly detection — only loaded when ml: true */
49
+ isolationForest;
50
+ /** Transaction graph — only loaded when ml: true */
51
+ transactionGraph;
52
+ /** Behavioral fingerprinting — only loaded when ml: true */
53
+ behaviorProfile;
54
+ constructor(config) {
55
+ this.config = { ...exports.DEFAULT_FRAUD_CONFIG, ...config };
56
+ if (this.config.ml) {
57
+ this.isolationForest = new fraud_ml_js_1.IsolationForest();
58
+ this.transactionGraph = new fraud_ml_js_1.TransactionGraph();
59
+ this.behaviorProfile = new fraud_ml_js_1.BehaviorProfile();
60
+ }
61
+ else {
62
+ this.isolationForest = null;
63
+ this.transactionGraph = null;
64
+ this.behaviorProfile = null;
65
+ }
66
+ }
67
+ // ── Risk Assessment ────────────────────────────────────────────────────
68
+ /**
69
+ * Assess risk for a charge attempt. Returns a RiskAssessment
70
+ * that includes whether the charge should proceed.
71
+ */
72
+ assessCharge(agentId, amount, reputation, accountCreatedAt, pendingCount, ctx) {
73
+ const signals = [];
74
+ // 0. Hard block check
75
+ if (this.blockedAgents.has(agentId)) {
76
+ return {
77
+ score: 1.0,
78
+ level: "blocked",
79
+ signals: [{ type: "agent_blocked", severity: "critical", description: "Agent is blocked", weight: 1.0 }],
80
+ allowed: false,
81
+ reason: "Agent has been blocked due to fraud violations",
82
+ flagged: false,
83
+ };
84
+ }
85
+ // 1. Velocity checks
86
+ const now = Date.now();
87
+ const history = this.chargeHistory.get(agentId) || [];
88
+ const chargesLastMinute = history.filter((e) => now - e.timestamp < 60_000).length;
89
+ if (chargesLastMinute >= this.config.maxChargesPerMinute) {
90
+ signals.push({
91
+ type: "velocity_burst",
92
+ severity: "high",
93
+ description: `${chargesLastMinute} charges in last minute (limit: ${this.config.maxChargesPerMinute})`,
94
+ weight: 0.8,
95
+ });
96
+ }
97
+ const chargesLastHour = history.filter((e) => now - e.timestamp < 3_600_000).length;
98
+ if (chargesLastHour >= this.config.maxChargesPerHour) {
99
+ signals.push({
100
+ type: "velocity_hourly",
101
+ severity: "high",
102
+ description: `${chargesLastHour} charges in last hour (limit: ${this.config.maxChargesPerHour})`,
103
+ weight: 0.7,
104
+ });
105
+ }
106
+ const chargesLastDay = history.filter((e) => now - e.timestamp < 86_400_000).length;
107
+ if (chargesLastDay >= this.config.maxChargesPerDay) {
108
+ signals.push({
109
+ type: "velocity_daily",
110
+ severity: "medium",
111
+ description: `${chargesLastDay} charges in last 24h (limit: ${this.config.maxChargesPerDay})`,
112
+ weight: 0.6,
113
+ });
114
+ }
115
+ // 2. Daily volume check
116
+ const dayVolume = history
117
+ .filter((e) => now - e.timestamp < 86_400_000)
118
+ .reduce((sum, e) => sum + e.amount, 0);
119
+ if (dayVolume + amount > this.config.maxDailyVolume) {
120
+ signals.push({
121
+ type: "volume_limit",
122
+ severity: "high",
123
+ description: `Daily volume $${(dayVolume + amount).toFixed(2)} exceeds limit $${this.config.maxDailyVolume}`,
124
+ weight: 0.7,
125
+ });
126
+ }
127
+ // 3. Amount anomaly detection
128
+ const stats = this.agentStats.get(agentId);
129
+ // ML Isolation Forest (only when ml: true)
130
+ if (this.isolationForest) {
131
+ const iforestScore = this.isolationForest.score([
132
+ amount, new Date().getHours(), 0, history.filter((e) => now - e.timestamp < 600_000).length,
133
+ stats ? stats.sum / Math.max(stats.count, 1) : 0, 0, pendingCount, reputation,
134
+ ]);
135
+ if (iforestScore >= 0 && iforestScore > 0.65) {
136
+ signals.push({
137
+ type: "ml_anomaly",
138
+ severity: iforestScore > 0.8 ? "high" : "medium",
139
+ description: `ML anomaly score ${iforestScore.toFixed(2)} (Isolation Forest)`,
140
+ weight: Math.min(iforestScore * 0.9, 0.85),
141
+ });
142
+ }
143
+ }
144
+ // z-score (always runs — lightweight, no ML dependency)
145
+ if (stats && stats.count >= 5) {
146
+ const mean = stats.sum / stats.count;
147
+ const variance = stats.sumSq / stats.count - mean * mean;
148
+ const stdDev = Math.sqrt(Math.max(variance, 0));
149
+ if (stdDev > 0) {
150
+ const zScore = (amount - mean) / stdDev;
151
+ if (zScore > this.config.anomalyStdDevThreshold) {
152
+ signals.push({
153
+ type: "amount_anomaly",
154
+ severity: "medium",
155
+ description: `Amount $${amount.toFixed(2)} is ${zScore.toFixed(1)} std devs above mean $${mean.toFixed(2)}`,
156
+ weight: Math.min(0.3 + zScore * 0.1, 0.8),
157
+ });
158
+ }
159
+ }
160
+ }
161
+ // 3b. Behavioral drift detection (only when ml: true)
162
+ if (this.behaviorProfile) {
163
+ const driftSignals = this.behaviorProfile.detectDrift(agentId, amount);
164
+ for (const ds of driftSignals) {
165
+ signals.push({
166
+ type: `drift:${ds.type}`,
167
+ severity: ds.severity > 0.6 ? "high" : ds.severity > 0.3 ? "medium" : "low",
168
+ description: ds.description,
169
+ weight: ds.severity,
170
+ });
171
+ }
172
+ }
173
+ // 4. New agent high charge
174
+ const accountAgeMinutes = (now - accountCreatedAt.getTime()) / 60_000;
175
+ if (accountAgeMinutes < 60 && amount > 50) {
176
+ signals.push({
177
+ type: "new_agent_high_charge",
178
+ severity: "medium",
179
+ description: `Agent is ${Math.round(accountAgeMinutes)}min old, charging $${amount.toFixed(2)}`,
180
+ weight: 0.4,
181
+ });
182
+ }
183
+ // 5. Minimum account age
184
+ if (accountAgeMinutes < this.config.minAccountAgeMinutes) {
185
+ signals.push({
186
+ type: "account_too_new",
187
+ severity: "high",
188
+ description: `Account age ${Math.round(accountAgeMinutes)}min < required ${this.config.minAccountAgeMinutes}min`,
189
+ weight: 0.9,
190
+ });
191
+ }
192
+ // 6. Too many pending transactions
193
+ if (pendingCount >= this.config.maxPendingTransactions) {
194
+ signals.push({
195
+ type: "pending_overflow",
196
+ severity: "medium",
197
+ description: `${pendingCount} pending transactions (limit: ${this.config.maxPendingTransactions})`,
198
+ weight: 0.5,
199
+ });
200
+ }
201
+ // 7. Low reputation + high amount
202
+ if (reputation < 0.3 && amount > 100) {
203
+ signals.push({
204
+ type: "low_rep_high_charge",
205
+ severity: "high",
206
+ description: `Low reputation (${reputation.toFixed(2)}) attempting $${amount.toFixed(2)} charge`,
207
+ weight: 0.6,
208
+ });
209
+ }
210
+ // 8. Escalation pattern — progressively increasing amounts
211
+ const recentAmounts = history
212
+ .filter((e) => now - e.timestamp < 3_600_000)
213
+ .map((e) => e.amount);
214
+ if (recentAmounts.length >= 3) {
215
+ let escalating = true;
216
+ for (let i = 1; i < recentAmounts.length; i++) {
217
+ if (recentAmounts[i] <= recentAmounts[i - 1]) {
218
+ escalating = false;
219
+ break;
220
+ }
221
+ }
222
+ if (escalating && amount > recentAmounts[recentAmounts.length - 1]) {
223
+ signals.push({
224
+ type: "escalation_pattern",
225
+ severity: "medium",
226
+ description: `Escalating charge pattern detected: ${recentAmounts.map((a) => `$${a.toFixed(0)}`).join(" → ")} → $${amount.toFixed(0)}`,
227
+ weight: 0.4,
228
+ });
229
+ }
230
+ }
231
+ // 9. IP/geo checks
232
+ if (ctx?.country && this.config.blockedCountries.includes(ctx.country)) {
233
+ signals.push({
234
+ type: "blocked_country",
235
+ severity: "critical",
236
+ description: `Request from blocked country: ${ctx.country}`,
237
+ weight: 0.9,
238
+ });
239
+ }
240
+ if (ctx?.ip) {
241
+ const knownIps = this.agentIps.get(agentId);
242
+ if (knownIps && knownIps.size > 0 && !knownIps.has(ctx.ip)) {
243
+ // New IP for this agent
244
+ if (knownIps.size >= 5) {
245
+ signals.push({
246
+ type: "ip_hopping",
247
+ severity: "medium",
248
+ description: `Agent using ${knownIps.size + 1}th unique IP`,
249
+ weight: 0.3,
250
+ });
251
+ }
252
+ }
253
+ }
254
+ // 10. Rapid charge-settle cycle detection
255
+ // (checked from history: if last N transactions were all settled within seconds)
256
+ const recentCompleted = history
257
+ .filter((e) => now - e.timestamp < 600_000) // last 10 min
258
+ .length;
259
+ if (recentCompleted >= 5 && chargesLastMinute >= 3) {
260
+ signals.push({
261
+ type: "rapid_cycle",
262
+ severity: "high",
263
+ description: "Rapid charge-settle cycling detected",
264
+ weight: 0.6,
265
+ });
266
+ }
267
+ // ── Compute composite score ─────────────────────────────────────────
268
+ const score = signals.length === 0
269
+ ? 0
270
+ : Math.min(signals.reduce((sum, s) => sum + s.weight, 0) /
271
+ Math.max(signals.length, 1),
272
+ // Also take the max single signal weight — one critical signal can block
273
+ Math.max(...signals.map((s) => s.weight)), 1.0);
274
+ // Use maximum of weighted average and max single signal
275
+ const compositeScore = Math.min(Math.max(signals.reduce((sum, s) => sum + s.weight, 0) / Math.max(signals.length * 0.7, 1), signals.length > 0 ? Math.max(...signals.map((s) => s.weight)) * 0.85 : 0), 1.0);
276
+ const level = compositeScore >= this.config.blockThreshold
277
+ ? "blocked"
278
+ : compositeScore >= this.config.flagThreshold
279
+ ? "high"
280
+ : compositeScore >= 0.25
281
+ ? "medium"
282
+ : compositeScore > 0.1
283
+ ? "low"
284
+ : "safe";
285
+ const allowed = compositeScore < this.config.blockThreshold;
286
+ const flagged = compositeScore >= this.config.flagThreshold && allowed;
287
+ if (flagged)
288
+ this.flaggedAgents.add(agentId);
289
+ return {
290
+ score: Math.round(compositeScore * 100) / 100,
291
+ level,
292
+ signals,
293
+ allowed,
294
+ reason: allowed ? undefined : `Blocked: risk score ${compositeScore.toFixed(2)} exceeds threshold ${this.config.blockThreshold}`,
295
+ flagged,
296
+ };
297
+ }
298
+ /**
299
+ * Record a successful charge for velocity tracking and stats.
300
+ * Call AFTER charge is approved.
301
+ */
302
+ recordCharge(agentId, amount, ctx) {
303
+ const now = Date.now();
304
+ // Update charge history
305
+ const history = this.chargeHistory.get(agentId) || [];
306
+ history.push({ timestamp: now, amount, agentId });
307
+ // Keep only last 48 hours
308
+ const cutoff = now - 48 * 3_600_000;
309
+ const filtered = history.filter((e) => e.timestamp > cutoff);
310
+ this.chargeHistory.set(agentId, filtered);
311
+ // Update running statistics (Welford's online algorithm)
312
+ const stats = this.agentStats.get(agentId) || { sum: 0, sumSq: 0, count: 0 };
313
+ stats.sum += amount;
314
+ stats.sumSq += amount * amount;
315
+ stats.count++;
316
+ this.agentStats.set(agentId, stats);
317
+ // Track IP
318
+ if (ctx?.ip) {
319
+ const ips = this.agentIps.get(agentId) || new Set();
320
+ ips.add(ctx.ip);
321
+ this.agentIps.set(agentId, ips);
322
+ }
323
+ // Feed ML systems (only when ml: true)
324
+ if (this.isolationForest) {
325
+ const recent10 = filtered.filter((e) => now - e.timestamp < 600_000);
326
+ const avgRecent = recent10.length > 0 ? recent10.reduce((s, e) => s + e.amount, 0) / recent10.length : 0;
327
+ const stdRecent = recent10.length > 1
328
+ ? Math.sqrt(recent10.reduce((s, e) => s + (e.amount - avgRecent) ** 2, 0) / recent10.length)
329
+ : 0;
330
+ this.isolationForest.addSample([
331
+ amount, new Date().getHours(), 0, recent10.length,
332
+ avgRecent, stdRecent, 0, 0.5,
333
+ ]);
334
+ }
335
+ if (this.behaviorProfile)
336
+ this.behaviorProfile.recordEvent(agentId, "charge", amount);
337
+ if (this.transactionGraph && ctx?.ip)
338
+ this.transactionGraph.registerAgent(agentId, ctx.ip);
339
+ }
340
+ /** Record a non-payment event (memory ops) for behavioral profiling */
341
+ recordEvent(agentId, type) {
342
+ if (this.behaviorProfile)
343
+ this.behaviorProfile.recordEvent(agentId, type);
344
+ }
345
+ /** Record a transaction between agents for graph analysis */
346
+ recordTransfer(fromAgent, toAgent, amount, txId) {
347
+ if (this.transactionGraph)
348
+ this.transactionGraph.addTransaction(fromAgent, toAgent, amount, txId);
349
+ }
350
+ /** Run collusion detection across the transaction graph (requires ml: true) */
351
+ detectCollusion() {
352
+ if (!this.transactionGraph)
353
+ return [];
354
+ return this.transactionGraph.detectAll();
355
+ }
356
+ /** Get an agent's behavioral baseline (requires ml: true) */
357
+ getAgentBaseline(agentId) {
358
+ if (!this.behaviorProfile)
359
+ return undefined;
360
+ return this.behaviorProfile.getBaseline(agentId);
361
+ }
362
+ // ── Platform Fee ──────────────────────────────────────────────────────
363
+ /**
364
+ * Calculate and record platform fee for a settlement.
365
+ * Returns { grossAmount, feeAmount, netAmount }.
366
+ */
367
+ applyPlatformFee(txId, agentId, grossAmount) {
368
+ const feeAmount = Math.round(grossAmount * this.config.platformFeeRate * 100) / 100;
369
+ const netAmount = Math.round((grossAmount - feeAmount) * 100) / 100;
370
+ const record = {
371
+ txId,
372
+ agentId,
373
+ grossAmount,
374
+ feeRate: this.config.platformFeeRate,
375
+ feeAmount,
376
+ netAmount,
377
+ createdAt: new Date(),
378
+ };
379
+ this.feeLedger.push(record);
380
+ this._platformFeesCollected += feeAmount;
381
+ return record;
382
+ }
383
+ /** Total platform fees collected */
384
+ get platformFeesCollected() {
385
+ return Math.round(this._platformFeesCollected * 100) / 100;
386
+ }
387
+ /** Get platform fee ledger */
388
+ getFeeLedger(limit = 50) {
389
+ return this.feeLedger.slice(-limit);
390
+ }
391
+ // ── Dispute Resolution ────────────────────────────────────────────────
392
+ /**
393
+ * File a dispute against a settled transaction.
394
+ * Returns the dispute if within the dispute window.
395
+ */
396
+ fileDispute(txId, agentId, reason, txCompletedAt, evidence) {
397
+ // Check dispute window
398
+ const minutesSinceSettlement = (Date.now() - txCompletedAt.getTime()) / 60_000;
399
+ if (minutesSinceSettlement > this.config.disputeWindowMinutes) {
400
+ throw new Error(`Dispute window expired. Transaction settled ${Math.round(minutesSinceSettlement)}min ago ` +
401
+ `(window: ${this.config.disputeWindowMinutes}min)`);
402
+ }
403
+ // Check for duplicate dispute
404
+ for (const d of this.disputes.values()) {
405
+ if (d.txId === txId && d.status === "open") {
406
+ throw new Error(`Active dispute already exists for transaction ${txId}`);
407
+ }
408
+ }
409
+ const dispute = {
410
+ id: crypto.randomUUID(),
411
+ txId,
412
+ agentId,
413
+ reason,
414
+ status: "open",
415
+ createdAt: new Date(),
416
+ evidence: evidence || [],
417
+ };
418
+ this.disputes.set(dispute.id, dispute);
419
+ return dispute;
420
+ }
421
+ /**
422
+ * Resolve a dispute. Either refund the transaction or uphold it.
423
+ */
424
+ resolveDispute(disputeId, outcome) {
425
+ const dispute = this.disputes.get(disputeId);
426
+ if (!dispute)
427
+ throw new Error(`Dispute ${disputeId} not found`);
428
+ if (dispute.status !== "open")
429
+ throw new Error(`Dispute ${disputeId} is already ${dispute.status}`);
430
+ dispute.status = outcome === "refund" ? "resolved_refunded" : "resolved_upheld";
431
+ dispute.resolvedAt = new Date();
432
+ // If refund outcome, flag agent for review
433
+ if (outcome === "refund") {
434
+ this.flaggedAgents.add(dispute.agentId);
435
+ }
436
+ return dispute;
437
+ }
438
+ /** Get all disputes for an agent */
439
+ getDisputes(agentId) {
440
+ const all = Array.from(this.disputes.values());
441
+ if (agentId)
442
+ return all.filter((d) => d.agentId === agentId);
443
+ return all;
444
+ }
445
+ /** Get a specific dispute */
446
+ getDispute(disputeId) {
447
+ return this.disputes.get(disputeId);
448
+ }
449
+ /**
450
+ * Check if a transaction is within the settlement hold period.
451
+ * Returns true if the transaction is still held and cannot be withdrawn.
452
+ */
453
+ isWithinHoldPeriod(settledAt) {
454
+ const minutesSince = (Date.now() - settledAt.getTime()) / 60_000;
455
+ return minutesSince < this.config.settlementHoldMinutes;
456
+ }
457
+ /** Check if a transaction is still within the dispute window */
458
+ isWithinDisputeWindow(settledAt) {
459
+ const minutesSince = (Date.now() - settledAt.getTime()) / 60_000;
460
+ return minutesSince < this.config.disputeWindowMinutes;
461
+ }
462
+ // ── Agent Management ──────────────────────────────────────────────────
463
+ /** Block an agent from making any charges */
464
+ blockAgent(agentId) {
465
+ this.blockedAgents.add(agentId);
466
+ }
467
+ /** Unblock a previously blocked agent */
468
+ unblockAgent(agentId) {
469
+ this.blockedAgents.delete(agentId);
470
+ }
471
+ /** Check if an agent is blocked */
472
+ isBlocked(agentId) {
473
+ return this.blockedAgents.has(agentId);
474
+ }
475
+ /** Check if an agent is flagged */
476
+ isFlagged(agentId) {
477
+ return this.flaggedAgents.has(agentId);
478
+ }
479
+ /** Get fraud stats summary */
480
+ stats() {
481
+ return {
482
+ totalChargesTracked: Array.from(this.chargeHistory.values()).reduce((sum, h) => sum + h.length, 0),
483
+ agentsTracked: this.chargeHistory.size,
484
+ agentsFlagged: this.flaggedAgents.size,
485
+ agentsBlocked: this.blockedAgents.size,
486
+ openDisputes: Array.from(this.disputes.values()).filter((d) => d.status === "open").length,
487
+ platformFeesCollected: this.platformFeesCollected,
488
+ };
489
+ }
490
+ // ── Serialization (for persistence) ───────────────────────────────────
491
+ serialize() {
492
+ return JSON.stringify({
493
+ chargeHistory: Array.from(this.chargeHistory.entries()),
494
+ agentStats: Array.from(this.agentStats.entries()),
495
+ disputes: Array.from(this.disputes.entries()).map(([k, v]) => [k, { ...v, createdAt: v.createdAt.toISOString(), resolvedAt: v.resolvedAt?.toISOString() }]),
496
+ feeLedger: this.feeLedger.map((f) => ({ ...f, createdAt: f.createdAt.toISOString() })),
497
+ platformFeesCollected: this._platformFeesCollected,
498
+ agentIps: Array.from(this.agentIps.entries()).map(([k, v]) => [k, Array.from(v)]),
499
+ flaggedAgents: Array.from(this.flaggedAgents),
500
+ blockedAgents: Array.from(this.blockedAgents),
501
+ isolationForest: this.isolationForest?.serialize() ?? null,
502
+ transactionGraph: this.transactionGraph?.serialize() ?? null,
503
+ behaviorProfile: this.behaviorProfile?.serialize() ?? null,
504
+ });
505
+ }
506
+ static deserialize(json, config) {
507
+ const guard = new FraudGuard(config);
508
+ try {
509
+ const data = JSON.parse(json);
510
+ if (data.chargeHistory) {
511
+ guard.chargeHistory = new Map(data.chargeHistory);
512
+ }
513
+ if (data.agentStats) {
514
+ guard.agentStats = new Map(data.agentStats);
515
+ }
516
+ if (data.disputes) {
517
+ guard.disputes = new Map(data.disputes.map(([k, v]) => [
518
+ k,
519
+ { ...v, createdAt: new Date(v.createdAt), resolvedAt: v.resolvedAt ? new Date(v.resolvedAt) : undefined },
520
+ ]));
521
+ }
522
+ if (data.feeLedger) {
523
+ guard.feeLedger = data.feeLedger.map((f) => ({ ...f, createdAt: new Date(f.createdAt) }));
524
+ }
525
+ if (data.platformFeesCollected !== undefined) {
526
+ guard._platformFeesCollected = data.platformFeesCollected;
527
+ }
528
+ if (data.agentIps) {
529
+ guard.agentIps = new Map(data.agentIps.map(([k, v]) => [k, new Set(v)]));
530
+ }
531
+ if (data.flaggedAgents) {
532
+ guard.flaggedAgents = new Set(data.flaggedAgents);
533
+ }
534
+ if (data.blockedAgents) {
535
+ guard.blockedAgents = new Set(data.blockedAgents);
536
+ }
537
+ if (guard.config.ml && data.isolationForest) {
538
+ guard.isolationForest = fraud_ml_js_1.IsolationForest.deserialize(data.isolationForest);
539
+ }
540
+ if (guard.config.ml && data.transactionGraph) {
541
+ guard.transactionGraph = fraud_ml_js_1.TransactionGraph.deserialize(data.transactionGraph);
542
+ }
543
+ if (guard.config.ml && data.behaviorProfile) {
544
+ guard.behaviorProfile = fraud_ml_js_1.BehaviorProfile.deserialize(data.behaviorProfile);
545
+ }
546
+ }
547
+ catch {
548
+ // Return fresh guard if deserialization fails
549
+ }
550
+ return guard;
551
+ }
552
+ }
553
+ exports.FraudGuard = FraudGuard;
554
+ exports.DEFAULT_RATE_LIMIT = {
555
+ maxRequests: 60,
556
+ windowMs: 60_000,
557
+ maxPaymentRequests: 10,
558
+ paymentWindowMs: 60_000,
559
+ };
560
+ class RateLimiter {
561
+ config;
562
+ /** IP → timestamps of recent requests */
563
+ requests = new Map();
564
+ /** IP → timestamps of recent payment operations */
565
+ paymentRequests = new Map();
566
+ constructor(config) {
567
+ this.config = { ...exports.DEFAULT_RATE_LIMIT, ...config };
568
+ }
569
+ /**
570
+ * Check if a request should be allowed.
571
+ * Returns { allowed, remaining, retryAfterMs? }
572
+ */
573
+ check(key, isPayment = false) {
574
+ const now = Date.now();
575
+ // General rate limit
576
+ const reqs = this.requests.get(key) || [];
577
+ const windowStart = now - this.config.windowMs;
578
+ const recentReqs = reqs.filter((t) => t > windowStart);
579
+ this.requests.set(key, recentReqs);
580
+ if (recentReqs.length >= this.config.maxRequests) {
581
+ const oldestInWindow = recentReqs[0];
582
+ return {
583
+ allowed: false,
584
+ remaining: 0,
585
+ retryAfterMs: oldestInWindow + this.config.windowMs - now,
586
+ };
587
+ }
588
+ // Payment-specific rate limit
589
+ if (isPayment) {
590
+ const payReqs = this.paymentRequests.get(key) || [];
591
+ const payWindowStart = now - this.config.paymentWindowMs;
592
+ const recentPayReqs = payReqs.filter((t) => t > payWindowStart);
593
+ this.paymentRequests.set(key, recentPayReqs);
594
+ if (recentPayReqs.length >= this.config.maxPaymentRequests) {
595
+ const oldestPay = recentPayReqs[0];
596
+ return {
597
+ allowed: false,
598
+ remaining: 0,
599
+ retryAfterMs: oldestPay + this.config.paymentWindowMs - now,
600
+ };
601
+ }
602
+ recentPayReqs.push(now);
603
+ this.paymentRequests.set(key, recentPayReqs);
604
+ }
605
+ recentReqs.push(now);
606
+ this.requests.set(key, recentReqs);
607
+ const remaining = isPayment
608
+ ? Math.min(this.config.maxRequests - recentReqs.length, this.config.maxPaymentRequests - (this.paymentRequests.get(key)?.length || 0))
609
+ : this.config.maxRequests - recentReqs.length;
610
+ return { allowed: true, remaining };
611
+ }
612
+ /** Clean up old entries (call periodically) */
613
+ cleanup() {
614
+ const now = Date.now();
615
+ const cutoff = now - Math.max(this.config.windowMs, this.config.paymentWindowMs) * 2;
616
+ for (const [key, reqs] of this.requests) {
617
+ const filtered = reqs.filter((t) => t > cutoff);
618
+ if (filtered.length === 0)
619
+ this.requests.delete(key);
620
+ else
621
+ this.requests.set(key, filtered);
622
+ }
623
+ for (const [key, reqs] of this.paymentRequests) {
624
+ const filtered = reqs.filter((t) => t > cutoff);
625
+ if (filtered.length === 0)
626
+ this.paymentRequests.delete(key);
627
+ else
628
+ this.paymentRequests.set(key, filtered);
629
+ }
630
+ }
631
+ }
632
+ exports.RateLimiter = RateLimiter;
633
+ //# sourceMappingURL=fraud.js.map