chargeback-guard 2.0.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/LICENSE +21 -0
- package/README.md +311 -0
- package/docs/api.md +278 -0
- package/docs/architecture.md +281 -0
- package/docs/configuration.md +292 -0
- package/docs/getting-started.md +155 -0
- package/examples/advancedConfig.ts +123 -0
- package/examples/basicUsage.ts +98 -0
- package/examples/stripeIntegration.ts +106 -0
- package/package.json +181 -0
- package/src/ai/fraudDetection.ts +261 -0
- package/src/ai/patternRecognition.ts +218 -0
- package/src/analytics/dashboard.ts +195 -0
- package/src/analytics/metrics.ts +175 -0
- package/src/analytics/predictions.ts +135 -0
- package/src/analytics/reports.ts +221 -0
- package/src/api/controllers.ts +339 -0
- package/src/api/middleware.ts +172 -0
- package/src/api/routes.ts +141 -0
- package/src/config.ts +231 -0
- package/src/core/chargebackGuard.ts +616 -0
- package/src/core/eventEmitter.ts +118 -0
- package/src/core/lifecycle.ts +215 -0
- package/src/database/schema.ts +392 -0
- package/src/dispute/analyzer.ts +317 -0
- package/src/dispute/bankIntegration.ts +274 -0
- package/src/dispute/detector.ts +239 -0
- package/src/dispute/responseEngine.ts +440 -0
- package/src/evidence/collector.ts +426 -0
- package/src/evidence/encryption.ts +168 -0
- package/src/evidence/storage.ts +197 -0
- package/src/evidence/validator.ts +184 -0
- package/src/index.ts +43 -0
- package/src/integrations/paypal.ts +258 -0
- package/src/integrations/stripe.ts +280 -0
- package/src/integrations/webhook.ts +332 -0
- package/src/notifications/email.ts +161 -0
- package/src/notifications/inApp.ts +319 -0
- package/src/notifications/sms.ts +58 -0
- package/src/security/auth.ts +153 -0
- package/src/security/rateLimit.ts +77 -0
- package/src/security/validation.ts +166 -0
- package/src/server.ts +122 -0
- package/src/types/index.ts +790 -0
- package/src/utils/formatters.ts +72 -0
- package/src/utils/helpers.ts +193 -0
- package/src/utils/logger.ts +88 -0
- package/src/utils/validators.ts +39 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// CHARGEBACK GUARD — Evidence Collector
|
|
3
|
+
// Gathers all data points to build an airtight chargeback defense
|
|
4
|
+
// ============================================================
|
|
5
|
+
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
import { createLogger } from '../utils/logger';
|
|
8
|
+
import {
|
|
9
|
+
Evidence,
|
|
10
|
+
EvidenceData,
|
|
11
|
+
CustomerEvidenceData,
|
|
12
|
+
TransactionEvidenceData,
|
|
13
|
+
ShippingEvidenceData,
|
|
14
|
+
InteractionEvidenceData,
|
|
15
|
+
DeviceEvidenceData,
|
|
16
|
+
EmailEvidenceData,
|
|
17
|
+
CustomerHistoryData,
|
|
18
|
+
PaymentLogData,
|
|
19
|
+
GeolocationData,
|
|
20
|
+
RiskLevel,
|
|
21
|
+
} from '../types';
|
|
22
|
+
|
|
23
|
+
const log = createLogger('EvidenceCollector');
|
|
24
|
+
|
|
25
|
+
// ────────────────────────────────────────────────────────────
|
|
26
|
+
// COLLECTION CONTEXT (input to the collector)
|
|
27
|
+
// ────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export interface CollectionContext {
|
|
30
|
+
orderId: string;
|
|
31
|
+
customerIp?: string;
|
|
32
|
+
userAgent?: string;
|
|
33
|
+
acceptLanguage?: string;
|
|
34
|
+
referer?: string;
|
|
35
|
+
sessionData?: {
|
|
36
|
+
sessionId?: string;
|
|
37
|
+
duration?: number;
|
|
38
|
+
pageViews?: Array<{ url: string; title?: string; duration: number; timestamp: string }>;
|
|
39
|
+
clickTracking?: Array<{ element: string; x: number; y: number; timestamp: string }>;
|
|
40
|
+
formInteractions?: Array<{ field: string; action: string; timestamp: string }>;
|
|
41
|
+
mouseTracking?: Array<{ x: number; y: number; timestamp: string }>;
|
|
42
|
+
scrollDepth?: number;
|
|
43
|
+
timeOnCheckout?: number;
|
|
44
|
+
referrer?: string;
|
|
45
|
+
utmSource?: string;
|
|
46
|
+
utmMedium?: string;
|
|
47
|
+
utmCampaign?: string;
|
|
48
|
+
};
|
|
49
|
+
deviceFingerprint?: {
|
|
50
|
+
screenResolution?: string;
|
|
51
|
+
timezone?: string;
|
|
52
|
+
language?: string;
|
|
53
|
+
plugins?: string[];
|
|
54
|
+
fonts?: string[];
|
|
55
|
+
webgl?: string;
|
|
56
|
+
canvas?: string;
|
|
57
|
+
cookiesEnabled?: boolean;
|
|
58
|
+
doNotTrack?: string;
|
|
59
|
+
platform?: string;
|
|
60
|
+
hardwareConcurrency?: number;
|
|
61
|
+
deviceMemory?: number;
|
|
62
|
+
touchPoints?: number;
|
|
63
|
+
};
|
|
64
|
+
amount?: number;
|
|
65
|
+
currency?: string;
|
|
66
|
+
transactionId?: string;
|
|
67
|
+
paymentMethod?: string;
|
|
68
|
+
cardLastFour?: string;
|
|
69
|
+
cardBrand?: string;
|
|
70
|
+
shippingAddress?: Record<string, string>;
|
|
71
|
+
billingAddress?: Record<string, string>;
|
|
72
|
+
customerEmail?: string;
|
|
73
|
+
customerId?: string;
|
|
74
|
+
trackingNumber?: string;
|
|
75
|
+
estimatedDelivery?: string;
|
|
76
|
+
emailConfirmation?: {
|
|
77
|
+
sentAt: string;
|
|
78
|
+
opened?: boolean;
|
|
79
|
+
openedAt?: string;
|
|
80
|
+
clickedLinks?: string[];
|
|
81
|
+
};
|
|
82
|
+
customerHistory?: {
|
|
83
|
+
accountCreatedAt?: string;
|
|
84
|
+
previousPurchases?: number;
|
|
85
|
+
previousDisputes?: number;
|
|
86
|
+
riskScore?: number;
|
|
87
|
+
trustScore?: number;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ────────────────────────────────────────────────────────────
|
|
92
|
+
// EVIDENCE COLLECTOR CLASS
|
|
93
|
+
// ────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
export class EvidenceCollector {
|
|
96
|
+
|
|
97
|
+
private readonly strategies: {
|
|
98
|
+
[key: string]: (ctx: CollectionContext) => Promise<unknown>;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
constructor() {
|
|
102
|
+
this.strategies = {
|
|
103
|
+
customerData: this._collectCustomerData.bind(this),
|
|
104
|
+
transactionData: this._collectTransactionData.bind(this),
|
|
105
|
+
shippingData: this._collectShippingData.bind(this),
|
|
106
|
+
interactionData: this._collectInteractionData.bind(this),
|
|
107
|
+
deviceFingerprint: this._collectDeviceFingerprint.bind(this),
|
|
108
|
+
emailConfirmation: this._collectEmailConfirmation.bind(this),
|
|
109
|
+
customerHistory: this._collectCustomerHistory.bind(this),
|
|
110
|
+
paymentLog: this._collectPaymentLog.bind(this),
|
|
111
|
+
geolocation: this._collectGeolocation.bind(this),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ──────────────────────────────────────────
|
|
116
|
+
// MAIN COLLECT METHOD
|
|
117
|
+
// ──────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
async collect(ctx: CollectionContext): Promise<Evidence> {
|
|
120
|
+
log.debug(`Collecting evidence for order: ${ctx.orderId}`);
|
|
121
|
+
|
|
122
|
+
const collectionId = this._generateId();
|
|
123
|
+
const data: Partial<EvidenceData> = {};
|
|
124
|
+
const errors: string[] = [];
|
|
125
|
+
|
|
126
|
+
for (const [key, strategy] of Object.entries(this.strategies)) {
|
|
127
|
+
try {
|
|
128
|
+
(data as Record<string, unknown>)[key] = await strategy(ctx);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
131
|
+
errors.push(`${key}: ${msg}`);
|
|
132
|
+
log.warn(`Evidence strategy failed: ${key}`, { error: msg });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const trustScore = this._calculateTrustScore(data as EvidenceData);
|
|
137
|
+
const riskLevel = this._trustToRiskLevel(trustScore);
|
|
138
|
+
|
|
139
|
+
if (errors.length > 0) {
|
|
140
|
+
log.warn(`Evidence collection completed with ${errors.length} partial failures`, { errors });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const evidence: Evidence = {
|
|
144
|
+
collectionId,
|
|
145
|
+
orderId: ctx.orderId,
|
|
146
|
+
timestamp: new Date().toISOString(),
|
|
147
|
+
trustScore,
|
|
148
|
+
riskLevel,
|
|
149
|
+
data: data as EvidenceData,
|
|
150
|
+
metadata: {
|
|
151
|
+
collectionErrors: errors,
|
|
152
|
+
strategiesRun: Object.keys(this.strategies).length,
|
|
153
|
+
strategiesSucceeded: Object.keys(this.strategies).length - errors.length,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
log.info(`Evidence collected for order: ${ctx.orderId}`, {
|
|
158
|
+
trustScore,
|
|
159
|
+
riskLevel,
|
|
160
|
+
collectionId,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return evidence;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ──────────────────────────────────────────
|
|
167
|
+
// STRATEGY: Customer Data
|
|
168
|
+
// ──────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
private async _collectCustomerData(ctx: CollectionContext): Promise<CustomerEvidenceData> {
|
|
171
|
+
return {
|
|
172
|
+
ipAddress: ctx.customerIp ?? '0.0.0.0',
|
|
173
|
+
userAgent: ctx.userAgent ?? 'Unknown',
|
|
174
|
+
acceptLanguage: ctx.acceptLanguage ?? 'Unknown',
|
|
175
|
+
referer: ctx.referer ?? 'direct',
|
|
176
|
+
deviceId: this._generateDeviceId(ctx),
|
|
177
|
+
email: ctx.customerEmail,
|
|
178
|
+
ipReputation: await this._getIpReputation(ctx.customerIp),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ──────────────────────────────────────────
|
|
183
|
+
// STRATEGY: Transaction Data
|
|
184
|
+
// ──────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
private async _collectTransactionData(ctx: CollectionContext): Promise<TransactionEvidenceData> {
|
|
187
|
+
return {
|
|
188
|
+
timestamp: new Date().toISOString(),
|
|
189
|
+
amount: ctx.amount ?? 0,
|
|
190
|
+
currency: ctx.currency ?? 'USD',
|
|
191
|
+
paymentMethod: ctx.paymentMethod,
|
|
192
|
+
cardLastFour: ctx.cardLastFour
|
|
193
|
+
? this._maskCard(ctx.cardLastFour)
|
|
194
|
+
: undefined,
|
|
195
|
+
transactionId: ctx.transactionId ?? this._generateId(),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ──────────────────────────────────────────
|
|
200
|
+
// STRATEGY: Shipping Data
|
|
201
|
+
// ──────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
private async _collectShippingData(ctx: CollectionContext): Promise<ShippingEvidenceData> {
|
|
204
|
+
return {
|
|
205
|
+
address: ctx.shippingAddress as ShippingEvidenceData['address'],
|
|
206
|
+
country: (ctx.shippingAddress?.country) ?? undefined,
|
|
207
|
+
zip: ctx.shippingAddress?.postalCode,
|
|
208
|
+
trackingNumber: ctx.trackingNumber,
|
|
209
|
+
estimatedDelivery: ctx.estimatedDelivery,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ──────────────────────────────────────────
|
|
214
|
+
// STRATEGY: Interaction Data
|
|
215
|
+
// ──────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
private async _collectInteractionData(ctx: CollectionContext): Promise<InteractionEvidenceData> {
|
|
218
|
+
const session = ctx.sessionData ?? {};
|
|
219
|
+
return {
|
|
220
|
+
sessionDuration: session.duration ?? 0,
|
|
221
|
+
pageViews: session.pageViews ?? [],
|
|
222
|
+
clickTracking: (session.clickTracking ?? []) as InteractionEvidenceData['clickTracking'],
|
|
223
|
+
formInteractions: (session.formInteractions ?? []) as InteractionEvidenceData['formInteractions'],
|
|
224
|
+
timeOnCheckout: session.timeOnCheckout ?? 0,
|
|
225
|
+
mouseTracking: session.mouseTracking,
|
|
226
|
+
scrollDepth: session.scrollDepth,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ──────────────────────────────────────────
|
|
231
|
+
// STRATEGY: Device Fingerprint
|
|
232
|
+
// ──────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
private async _collectDeviceFingerprint(ctx: CollectionContext): Promise<DeviceEvidenceData> {
|
|
235
|
+
const fp = ctx.deviceFingerprint ?? {};
|
|
236
|
+
const fingerprintStr = JSON.stringify({
|
|
237
|
+
ua: ctx.userAgent,
|
|
238
|
+
screen: fp.screenResolution,
|
|
239
|
+
tz: fp.timezone,
|
|
240
|
+
lang: fp.language,
|
|
241
|
+
platform: fp.platform,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
screenResolution: fp.screenResolution,
|
|
246
|
+
timezone: fp.timezone,
|
|
247
|
+
language: fp.language,
|
|
248
|
+
plugins: fp.plugins,
|
|
249
|
+
fonts: fp.fonts,
|
|
250
|
+
webgl: fp.webgl,
|
|
251
|
+
canvas: fp.canvas,
|
|
252
|
+
fingerprint: crypto.createHash('sha256').update(fingerprintStr).digest('hex'),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ──────────────────────────────────────────
|
|
257
|
+
// STRATEGY: Email Confirmation
|
|
258
|
+
// ──────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
private async _collectEmailConfirmation(ctx: CollectionContext): Promise<EmailEvidenceData> {
|
|
261
|
+
const ec = ctx.emailConfirmation;
|
|
262
|
+
return {
|
|
263
|
+
sentTo: ctx.customerEmail ?? '',
|
|
264
|
+
sentAt: ec?.sentAt ?? new Date().toISOString(),
|
|
265
|
+
opened: ec?.opened,
|
|
266
|
+
openedAt: ec?.openedAt,
|
|
267
|
+
clickedLinks: ec?.clickedLinks ?? [],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ──────────────────────────────────────────
|
|
272
|
+
// STRATEGY: Customer History
|
|
273
|
+
// ──────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
private async _collectCustomerHistory(ctx: CollectionContext): Promise<CustomerHistoryData> {
|
|
276
|
+
const history = ctx.customerHistory ?? {};
|
|
277
|
+
const createdAt = history.accountCreatedAt ?? new Date().toISOString();
|
|
278
|
+
const accountAgeDays = Math.floor(
|
|
279
|
+
(Date.now() - new Date(createdAt).getTime()) / (1000 * 60 * 60 * 24)
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
accountCreatedAt: createdAt,
|
|
284
|
+
accountAge: accountAgeDays,
|
|
285
|
+
previousPurchases: history.previousPurchases ?? 0,
|
|
286
|
+
previousDisputes: history.previousDisputes ?? 0,
|
|
287
|
+
riskScore: history.riskScore ?? 0.1,
|
|
288
|
+
trustScore: history.trustScore ?? 0.8,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ──────────────────────────────────────────
|
|
293
|
+
// STRATEGY: Payment Log
|
|
294
|
+
// ──────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
private async _collectPaymentLog(ctx: CollectionContext): Promise<PaymentLogData> {
|
|
297
|
+
return {
|
|
298
|
+
attempts: 1,
|
|
299
|
+
successful: 1,
|
|
300
|
+
failed: 0,
|
|
301
|
+
lastAttemptAt: new Date().toISOString(),
|
|
302
|
+
methods: [ctx.paymentMethod ?? 'card'],
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ──────────────────────────────────────────
|
|
307
|
+
// STRATEGY: Geolocation
|
|
308
|
+
// ──────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
private async _collectGeolocation(ctx: CollectionContext): Promise<GeolocationData | null> {
|
|
311
|
+
if (!ctx.customerIp) { return null; }
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
// geoip-lite provides offline lookup — no external API calls
|
|
315
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
316
|
+
const geoip = require('geoip-lite');
|
|
317
|
+
const geo = geoip.lookup(ctx.customerIp);
|
|
318
|
+
|
|
319
|
+
if (!geo) { return null; }
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
ip: ctx.customerIp,
|
|
323
|
+
country: geo.country ?? 'Unknown',
|
|
324
|
+
city: geo.city ?? 'Unknown',
|
|
325
|
+
timezone: geo.timezone ?? 'Unknown',
|
|
326
|
+
latitude: geo.ll?.[0],
|
|
327
|
+
longitude: geo.ll?.[1],
|
|
328
|
+
};
|
|
329
|
+
} catch {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ──────────────────────────────────────────
|
|
335
|
+
// TRUST SCORE CALCULATOR
|
|
336
|
+
// ──────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
private _calculateTrustScore(data: EvidenceData): number {
|
|
339
|
+
let score = 40; // baseline
|
|
340
|
+
|
|
341
|
+
// Customer data quality (+15 max)
|
|
342
|
+
const cd = data.customerData;
|
|
343
|
+
if (cd?.ipAddress && cd.ipAddress !== '0.0.0.0') { score += 5; }
|
|
344
|
+
if (cd?.userAgent && cd.userAgent !== 'Unknown') { score += 4; }
|
|
345
|
+
if (cd?.deviceId) { score += 6; }
|
|
346
|
+
|
|
347
|
+
// Transaction quality (+10 max)
|
|
348
|
+
const td = data.transactionData;
|
|
349
|
+
if (td?.timestamp) { score += 5; }
|
|
350
|
+
if (td?.transactionId) { score += 5; }
|
|
351
|
+
|
|
352
|
+
// Shipping quality (+20 max)
|
|
353
|
+
const sd = data.shippingData;
|
|
354
|
+
if (sd?.trackingNumber) { score += 15; }
|
|
355
|
+
if (sd?.address) { score += 5; }
|
|
356
|
+
|
|
357
|
+
// Interaction quality (+20 max)
|
|
358
|
+
const id = data.interactionData;
|
|
359
|
+
if (id) {
|
|
360
|
+
if (id.sessionDuration > 60) { score += 8; }
|
|
361
|
+
if (id.sessionDuration > 300) { score += 4; }
|
|
362
|
+
if (id.pageViews?.length > 3) { score += 5; }
|
|
363
|
+
if (id.timeOnCheckout > 30) { score += 3; }
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Email confirmation (+10 max)
|
|
367
|
+
const ec = data.emailConfirmation;
|
|
368
|
+
if (ec?.sentTo) { score += 5; }
|
|
369
|
+
if (ec?.opened === true) { score += 5; }
|
|
370
|
+
|
|
371
|
+
// Customer history (+15 max)
|
|
372
|
+
const ch = data.customerHistory;
|
|
373
|
+
if (ch) {
|
|
374
|
+
if (ch.accountAge > 90) { score += 5; }
|
|
375
|
+
if (ch.previousPurchases > 2) { score += 5; }
|
|
376
|
+
if (ch.previousDisputes === 0) { score += 5; }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// IP reputation adjustment
|
|
380
|
+
if (cd?.ipReputation !== undefined) {
|
|
381
|
+
if (cd.ipReputation < 0.3) { score -= 15; }
|
|
382
|
+
else if (cd.ipReputation < 0.5) { score -= 8; }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return Math.max(0, Math.min(100, Math.round(score)));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private _trustToRiskLevel(trust: number): RiskLevel {
|
|
389
|
+
if (trust >= 90) { return RiskLevel.VERY_LOW; }
|
|
390
|
+
if (trust >= 75) { return RiskLevel.LOW; }
|
|
391
|
+
if (trust >= 55) { return RiskLevel.MEDIUM; }
|
|
392
|
+
if (trust >= 35) { return RiskLevel.HIGH; }
|
|
393
|
+
return RiskLevel.CRITICAL;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ──────────────────────────────────────────
|
|
397
|
+
// HELPERS
|
|
398
|
+
// ──────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
private _generateId(): string {
|
|
401
|
+
return crypto.randomBytes(16).toString('hex');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private _generateDeviceId(ctx: CollectionContext): string {
|
|
405
|
+
const str = JSON.stringify({
|
|
406
|
+
ua: ctx.userAgent,
|
|
407
|
+
screen: ctx.deviceFingerprint?.screenResolution,
|
|
408
|
+
tz: ctx.deviceFingerprint?.timezone,
|
|
409
|
+
lang: ctx.deviceFingerprint?.language,
|
|
410
|
+
platform: ctx.deviceFingerprint?.platform,
|
|
411
|
+
});
|
|
412
|
+
return crypto.createHash('sha256').update(str).digest('hex');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private _maskCard(card: string): string {
|
|
416
|
+
if (card.length <= 4) { return card; }
|
|
417
|
+
return card.slice(-4).padStart(card.length, '*');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private async _getIpReputation(ip?: string): Promise<number> {
|
|
421
|
+
// In production this would call an IP reputation service
|
|
422
|
+
// For now return a safe default
|
|
423
|
+
if (!ip || ip === '127.0.0.1' || ip === '0.0.0.0') { return 0.9; }
|
|
424
|
+
return 0.75; // neutral score
|
|
425
|
+
}
|
|
426
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// CHARGEBACK GUARD — Evidence Encryption Module
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
import { createLogger } from '../utils/logger';
|
|
7
|
+
import { securityConfig } from '../config';
|
|
8
|
+
|
|
9
|
+
const log = createLogger('Encryption');
|
|
10
|
+
|
|
11
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
12
|
+
const IV_LENGTH = 12; // GCM recommended
|
|
13
|
+
const TAG_LENGTH = 16;
|
|
14
|
+
const KEY_LENGTH = 32;
|
|
15
|
+
|
|
16
|
+
// ────────────────────────────────────────────────────────────
|
|
17
|
+
// KEY DERIVATION
|
|
18
|
+
// ────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function deriveKey(secret: string): Buffer {
|
|
21
|
+
return crypto.scryptSync(secret, 'chargebackguard-salt-v2', KEY_LENGTH);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ────────────────────────────────────────────────────────────
|
|
25
|
+
// ENCRYPT
|
|
26
|
+
// ────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export function encrypt(plaintext: string, secret?: string): string {
|
|
29
|
+
const key = deriveKey(secret ?? securityConfig.encryptionKey);
|
|
30
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
31
|
+
|
|
32
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv) as crypto.CipherGCM;
|
|
33
|
+
const encrypted = Buffer.concat([
|
|
34
|
+
cipher.update(plaintext, 'utf8'),
|
|
35
|
+
cipher.final(),
|
|
36
|
+
]);
|
|
37
|
+
const tag = cipher.getAuthTag();
|
|
38
|
+
|
|
39
|
+
// Format: iv:tag:ciphertext (all hex)
|
|
40
|
+
return `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted.toString('hex')}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ────────────────────────────────────────────────────────────
|
|
44
|
+
// DECRYPT
|
|
45
|
+
// ────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export function decrypt(ciphertext: string, secret?: string): string {
|
|
48
|
+
const key = deriveKey(secret ?? securityConfig.encryptionKey);
|
|
49
|
+
const parts = ciphertext.split(':');
|
|
50
|
+
|
|
51
|
+
if (parts.length !== 3) {
|
|
52
|
+
throw new Error('Invalid ciphertext format');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const [ivHex, tagHex, encHex] = parts;
|
|
56
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
57
|
+
const tag = Buffer.from(tagHex, 'hex');
|
|
58
|
+
const encrypted = Buffer.from(encHex, 'hex');
|
|
59
|
+
|
|
60
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv) as crypto.DecipherGCM;
|
|
61
|
+
decipher.setAuthTag(tag);
|
|
62
|
+
|
|
63
|
+
return decipher.update(encrypted).toString('utf8') + decipher.final('utf8');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ────────────────────────────────────────────────────────────
|
|
67
|
+
// HASH (one-way, for indexing sensitive fields)
|
|
68
|
+
// ────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
export function hashField(value: string): string {
|
|
71
|
+
return crypto
|
|
72
|
+
.createHmac('sha256', securityConfig.encryptionKey)
|
|
73
|
+
.update(value.toLowerCase().trim())
|
|
74
|
+
.digest('hex');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ────────────────────────────────────────────────────────────
|
|
78
|
+
// ENCRYPT OBJECT (encrypts PII fields in place)
|
|
79
|
+
// ────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
const PII_FIELDS = [
|
|
82
|
+
'ipAddress', 'email', 'customerEmail', 'cardLastFour',
|
|
83
|
+
'phone', 'line1', 'line2', 'postalCode',
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
export function encryptPIIFields(
|
|
87
|
+
obj: Record<string, unknown>,
|
|
88
|
+
depth = 0
|
|
89
|
+
): Record<string, unknown> {
|
|
90
|
+
if (depth > 5) { return obj; } // guard against deep nesting
|
|
91
|
+
|
|
92
|
+
const result: Record<string, unknown> = {};
|
|
93
|
+
|
|
94
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
95
|
+
if (PII_FIELDS.includes(key) && typeof value === 'string' && value) {
|
|
96
|
+
result[key] = encrypt(value);
|
|
97
|
+
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
98
|
+
result[key] = encryptPIIFields(value as Record<string, unknown>, depth + 1);
|
|
99
|
+
} else {
|
|
100
|
+
result[key] = value;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function decryptPIIFields(
|
|
108
|
+
obj: Record<string, unknown>,
|
|
109
|
+
depth = 0
|
|
110
|
+
): Record<string, unknown> {
|
|
111
|
+
if (depth > 5) { return obj; }
|
|
112
|
+
|
|
113
|
+
const result: Record<string, unknown> = {};
|
|
114
|
+
|
|
115
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
116
|
+
if (PII_FIELDS.includes(key) && typeof value === 'string' && value.includes(':')) {
|
|
117
|
+
try {
|
|
118
|
+
result[key] = decrypt(value);
|
|
119
|
+
} catch {
|
|
120
|
+
log.warn(`Failed to decrypt field: ${key}`);
|
|
121
|
+
result[key] = value; // leave as-is
|
|
122
|
+
}
|
|
123
|
+
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
124
|
+
result[key] = decryptPIIFields(value as Record<string, unknown>, depth + 1);
|
|
125
|
+
} else {
|
|
126
|
+
result[key] = value;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ────────────────────────────────────────────────────────────
|
|
134
|
+
// GENERATE SECURE TOKENS
|
|
135
|
+
// ────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
export function generateApiKey(prefix = 'cbg_'): string {
|
|
138
|
+
const random = crypto.randomBytes(32).toString('base64url');
|
|
139
|
+
return `${prefix}${random}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function generateWebhookSecret(): string {
|
|
143
|
+
return crypto.randomBytes(32).toString('hex');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function generateSecureToken(length = 32): string {
|
|
147
|
+
return crypto.randomBytes(length).toString('base64url');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ────────────────────────────────────────────────────────────
|
|
151
|
+
// HMAC SIGNATURE (for webhooks)
|
|
152
|
+
// ────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
export function signPayload(payload: string, secret: string): string {
|
|
155
|
+
return crypto
|
|
156
|
+
.createHmac('sha256', secret)
|
|
157
|
+
.update(payload)
|
|
158
|
+
.digest('hex');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function verifySignature(payload: string, signature: string, secret: string): boolean {
|
|
162
|
+
const expected = signPayload(payload, secret);
|
|
163
|
+
const expectedBuf = Buffer.from(expected, 'hex');
|
|
164
|
+
const receivedBuf = Buffer.from(signature.replace('sha256=', ''), 'hex');
|
|
165
|
+
|
|
166
|
+
if (expectedBuf.length !== receivedBuf.length) { return false; }
|
|
167
|
+
return crypto.timingSafeEqual(expectedBuf, receivedBuf);
|
|
168
|
+
}
|