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,72 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// CHARGEBACK GUARD — Data Formatters
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
export function formatAmount(cents: number, currency = 'USD'): string {
|
|
6
|
+
return new Intl.NumberFormat('en-US', {
|
|
7
|
+
style: 'currency',
|
|
8
|
+
currency,
|
|
9
|
+
minimumFractionDigits: 2,
|
|
10
|
+
}).format(cents / 100);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatDate(date: string | Date, locale = 'en-US'): string {
|
|
14
|
+
return new Intl.DateTimeFormat(locale, {
|
|
15
|
+
year: 'numeric',
|
|
16
|
+
month: 'short',
|
|
17
|
+
day: 'numeric',
|
|
18
|
+
}).format(new Date(date));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatDateTime(date: string | Date, locale = 'en-US'): string {
|
|
22
|
+
return new Intl.DateTimeFormat(locale, {
|
|
23
|
+
year: 'numeric',
|
|
24
|
+
month: 'short',
|
|
25
|
+
day: 'numeric',
|
|
26
|
+
hour: '2-digit',
|
|
27
|
+
minute: '2-digit',
|
|
28
|
+
}).format(new Date(date));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function formatTrustScore(score: number): string {
|
|
32
|
+
if (score >= 90) { return `${score} (Excellent)`; }
|
|
33
|
+
if (score >= 75) { return `${score} (Good)`; }
|
|
34
|
+
if (score >= 55) { return `${score} (Fair)`; }
|
|
35
|
+
if (score >= 35) { return `${score} (Poor)`; }
|
|
36
|
+
return `${score} (Critical)`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatRiskLevel(level: string): string {
|
|
40
|
+
const labels: Record<string, string> = {
|
|
41
|
+
very_low: '🟢 Very Low',
|
|
42
|
+
low: '🟢 Low',
|
|
43
|
+
medium: '🟡 Medium',
|
|
44
|
+
high: '🟠 High',
|
|
45
|
+
critical: '🔴 Critical',
|
|
46
|
+
};
|
|
47
|
+
return labels[level] ?? level;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function formatDisputeReason(reason: string): string {
|
|
51
|
+
const labels: Record<string, string> = {
|
|
52
|
+
product_not_received: 'Product Not Received',
|
|
53
|
+
product_unacceptable: 'Product Unacceptable',
|
|
54
|
+
unauthorized_transaction: 'Unauthorized Transaction',
|
|
55
|
+
duplicate_transaction: 'Duplicate Transaction',
|
|
56
|
+
credit_not_processed: 'Credit Not Processed',
|
|
57
|
+
subscription_cancelled: 'Subscription Cancelled',
|
|
58
|
+
fraudulent: 'Fraudulent',
|
|
59
|
+
general: 'General',
|
|
60
|
+
};
|
|
61
|
+
return labels[reason] ?? reason.replace(/_/g, ' ');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function formatConfidenceLevel(level: string): string {
|
|
65
|
+
const labels: Record<string, string> = {
|
|
66
|
+
very_high: '✅ Very High (>85%)',
|
|
67
|
+
high: '👍 High (>65%)',
|
|
68
|
+
medium: '⚠️ Medium (>45%)',
|
|
69
|
+
low: '❌ Low (<45%)',
|
|
70
|
+
};
|
|
71
|
+
return labels[level] ?? level;
|
|
72
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// CHARGEBACK GUARD — General Helper Utilities
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
|
|
7
|
+
// ────────────────────────────────────────────────────────────
|
|
8
|
+
// STRING HELPERS
|
|
9
|
+
// ────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export function truncate(str: string, maxLength: number): string {
|
|
12
|
+
if (str.length <= maxLength) { return str; }
|
|
13
|
+
return str.slice(0, maxLength - 3) + '...';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function capitalize(str: string): string {
|
|
17
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function slugify(str: string): string {
|
|
21
|
+
return str.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function maskEmail(email: string): string {
|
|
25
|
+
const [local, domain] = email.split('@');
|
|
26
|
+
if (!local || !domain) { return email; }
|
|
27
|
+
const masked = local.slice(0, 2) + '*'.repeat(Math.max(0, local.length - 2));
|
|
28
|
+
return `${masked}@${domain}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ────────────────────────────────────────────────────────────
|
|
32
|
+
// NUMBER HELPERS
|
|
33
|
+
// ────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export function centsToDollars(cents: number): string {
|
|
36
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function dollarsToCents(dollars: number): number {
|
|
40
|
+
return Math.round(dollars * 100);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function formatPercentage(value: number, decimals = 1): string {
|
|
44
|
+
return `${value.toFixed(decimals)}%`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ────────────────────────────────────────────────────────────
|
|
48
|
+
// DATE HELPERS
|
|
49
|
+
// ────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export function daysFromNow(days: number): Date {
|
|
52
|
+
const d = new Date();
|
|
53
|
+
d.setDate(d.getDate() + days);
|
|
54
|
+
return d;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function daysBetween(from: Date, to: Date): number {
|
|
58
|
+
return Math.floor((to.getTime() - from.getTime()) / (1000 * 60 * 60 * 24));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isExpired(date: Date | string): boolean {
|
|
62
|
+
return new Date(date).getTime() < Date.now();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isDeadlineClose(date: Date | string, hoursThreshold = 48): boolean {
|
|
66
|
+
const remaining = new Date(date).getTime() - Date.now();
|
|
67
|
+
return remaining > 0 && remaining < hoursThreshold * 3600000;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function formatDuration(ms: number): string {
|
|
71
|
+
if (ms < 1000) { return `${ms}ms`; }
|
|
72
|
+
if (ms < 60000) { return `${(ms / 1000).toFixed(1)}s`; }
|
|
73
|
+
if (ms < 3600000) { return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`; }
|
|
74
|
+
return `${(ms / 3600000).toFixed(1)}h`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ────────────────────────────────────────────────────────────
|
|
78
|
+
// OBJECT HELPERS
|
|
79
|
+
// ────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
export function omit<T extends object, K extends keyof T>(
|
|
82
|
+
obj: T,
|
|
83
|
+
keys: K[]
|
|
84
|
+
): Omit<T, K> {
|
|
85
|
+
const result = { ...obj };
|
|
86
|
+
keys.forEach(key => delete result[key]);
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function pick<T extends object, K extends keyof T>(
|
|
91
|
+
obj: T,
|
|
92
|
+
keys: K[]
|
|
93
|
+
): Pick<T, K> {
|
|
94
|
+
const result = {} as Pick<T, K>;
|
|
95
|
+
keys.forEach(key => { result[key] = obj[key]; });
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function deepClone<T>(obj: T): T {
|
|
100
|
+
return JSON.parse(JSON.stringify(obj));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isEmpty(value: unknown): boolean {
|
|
104
|
+
if (value === null || value === undefined) { return true; }
|
|
105
|
+
if (typeof value === 'string') { return value.trim().length === 0; }
|
|
106
|
+
if (Array.isArray(value)) { return value.length === 0; }
|
|
107
|
+
if (typeof value === 'object') { return Object.keys(value).length === 0; }
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ────────────────────────────────────────────────────────────
|
|
112
|
+
// ARRAY HELPERS
|
|
113
|
+
// ────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
export function chunkArray<T>(arr: T[], size: number): T[][] {
|
|
116
|
+
const chunks: T[][] = [];
|
|
117
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
118
|
+
chunks.push(arr.slice(i, i + size));
|
|
119
|
+
}
|
|
120
|
+
return chunks;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function uniqueBy<T>(arr: T[], key: keyof T): T[] {
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
return arr.filter(item => {
|
|
126
|
+
const val = item[key];
|
|
127
|
+
if (seen.has(val)) { return false; }
|
|
128
|
+
seen.add(val);
|
|
129
|
+
return true;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function groupBy<T>(arr: T[], key: keyof T): Record<string, T[]> {
|
|
134
|
+
return arr.reduce((groups, item) => {
|
|
135
|
+
const k = String(item[key]);
|
|
136
|
+
if (!groups[k]) { groups[k] = []; }
|
|
137
|
+
groups[k].push(item);
|
|
138
|
+
return groups;
|
|
139
|
+
}, {} as Record<string, T[]>);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ────────────────────────────────────────────────────────────
|
|
143
|
+
// ASYNC HELPERS
|
|
144
|
+
// ────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
export async function sleep(ms: number): Promise<void> {
|
|
147
|
+
return new Promise(r => setTimeout(r, ms));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function retry<T>(
|
|
151
|
+
fn: () => Promise<T>,
|
|
152
|
+
retries = 3,
|
|
153
|
+
delayMs = 1000
|
|
154
|
+
): Promise<T> {
|
|
155
|
+
let lastError: Error | undefined;
|
|
156
|
+
for (let i = 0; i <= retries; i++) {
|
|
157
|
+
try {
|
|
158
|
+
return await fn();
|
|
159
|
+
} catch (err) {
|
|
160
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
161
|
+
if (i < retries) { await sleep(delayMs * Math.pow(2, i)); } // exponential backoff
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
throw lastError;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function timeout<T>(
|
|
168
|
+
promise: Promise<T>,
|
|
169
|
+
ms: number,
|
|
170
|
+
message = `Operation timed out after ${ms}ms`
|
|
171
|
+
): Promise<T> {
|
|
172
|
+
return Promise.race([
|
|
173
|
+
promise,
|
|
174
|
+
new Promise<never>((_, reject) =>
|
|
175
|
+
setTimeout(() => reject(new Error(message)), ms)
|
|
176
|
+
),
|
|
177
|
+
]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ────────────────────────────────────────────────────────────
|
|
181
|
+
// ID GENERATION
|
|
182
|
+
// ────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
export function generateId(prefix = ''): string {
|
|
185
|
+
const random = crypto.randomBytes(12).toString('base64url');
|
|
186
|
+
return prefix ? `${prefix}_${random}` : random;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function generateOrderId(): string {
|
|
190
|
+
const ts = Date.now().toString(36).toUpperCase();
|
|
191
|
+
const rand = crypto.randomBytes(4).toString('hex').toUpperCase();
|
|
192
|
+
return `ORD-${ts}-${rand}`;
|
|
193
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// CHARGEBACK GUARD — Logger Utility
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import winston from 'winston';
|
|
6
|
+
import DailyRotateFile from 'winston-daily-rotate-file';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { appConfig } from '../config';
|
|
9
|
+
|
|
10
|
+
const { combine, timestamp, printf, colorize, errors, json } = winston.format;
|
|
11
|
+
|
|
12
|
+
// ────────────────────────────────────────────────────────────
|
|
13
|
+
// CUSTOM FORMATS
|
|
14
|
+
// ────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const devFormat = combine(
|
|
17
|
+
colorize({ all: true }),
|
|
18
|
+
timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
|
|
19
|
+
errors({ stack: true }),
|
|
20
|
+
printf(({ level, message, timestamp: ts, ...meta }) => {
|
|
21
|
+
const metaStr = Object.keys(meta).length ? `\n${JSON.stringify(meta, null, 2)}` : '';
|
|
22
|
+
return `[${ts}] ${level}: ${message}${metaStr}`;
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const prodFormat = combine(
|
|
27
|
+
timestamp(),
|
|
28
|
+
errors({ stack: true }),
|
|
29
|
+
json()
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// ────────────────────────────────────────────────────────────
|
|
33
|
+
// TRANSPORTS
|
|
34
|
+
// ────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const transports: winston.transport[] = [
|
|
37
|
+
new winston.transports.Console({
|
|
38
|
+
format: appConfig.isDev ? devFormat : prodFormat,
|
|
39
|
+
silent: process.env.NODE_ENV === 'test',
|
|
40
|
+
}),
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// Add file transports in production / staging
|
|
44
|
+
if (!appConfig.isDev && process.env.NODE_ENV !== 'test') {
|
|
45
|
+
transports.push(
|
|
46
|
+
new DailyRotateFile({
|
|
47
|
+
filename: path.join('logs', 'application-%DATE%.log'),
|
|
48
|
+
datePattern: 'YYYY-MM-DD',
|
|
49
|
+
zippedArchive: true,
|
|
50
|
+
maxSize: '20m',
|
|
51
|
+
maxFiles: '30d',
|
|
52
|
+
format: prodFormat,
|
|
53
|
+
}),
|
|
54
|
+
new DailyRotateFile({
|
|
55
|
+
filename: path.join('logs', 'error-%DATE%.log'),
|
|
56
|
+
datePattern: 'YYYY-MM-DD',
|
|
57
|
+
zippedArchive: true,
|
|
58
|
+
maxSize: '20m',
|
|
59
|
+
maxFiles: '30d',
|
|
60
|
+
level: 'error',
|
|
61
|
+
format: prodFormat,
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ────────────────────────────────────────────────────────────
|
|
67
|
+
// LOGGER INSTANCE
|
|
68
|
+
// ────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
export const logger = winston.createLogger({
|
|
71
|
+
level: appConfig.logLevel,
|
|
72
|
+
defaultMeta: {
|
|
73
|
+
service: 'chargeback-guard',
|
|
74
|
+
version: appConfig.version,
|
|
75
|
+
},
|
|
76
|
+
transports,
|
|
77
|
+
exitOnError: false,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ────────────────────────────────────────────────────────────
|
|
81
|
+
// CHILD LOGGER FACTORY
|
|
82
|
+
// ────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
export const createLogger = (module: string): winston.Logger => {
|
|
85
|
+
return logger.child({ module });
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export default logger;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// CHARGEBACK GUARD — Validation Utility Functions
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
export function isValidEmail(email: string): boolean {
|
|
6
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isValidIP(ip: string): boolean {
|
|
10
|
+
const ipv4 = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
11
|
+
const ipv6 = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
|
|
12
|
+
return ipv4.test(ip) || ipv6.test(ip);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isValidCurrency(currency: string): boolean {
|
|
16
|
+
return /^[A-Z]{3}$/.test(currency);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isValidUrl(url: string): boolean {
|
|
20
|
+
try { new URL(url); return true; }
|
|
21
|
+
catch { return false; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isValidUUID(id: string): boolean {
|
|
25
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isPositiveAmount(amount: number): boolean {
|
|
29
|
+
return typeof amount === 'number' && amount > 0 && isFinite(amount);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function sanitizeString(str: string, maxLength = 255): string {
|
|
33
|
+
return str.trim().slice(0, maxLength).replace(/[<>]/g, '');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isValidDisputeId(id: string): boolean {
|
|
37
|
+
// Stripe: dp_xxx | PayPal: PP-D-xxx
|
|
38
|
+
return /^(dp_|PP-D-|ch_)[A-Za-z0-9_-]{10,}$/.test(id) || id.length >= 5;
|
|
39
|
+
}
|