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,77 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// CHARGEBACK GUARD — Rate Limiting
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import rateLimit from 'express-rate-limit';
|
|
6
|
+
import { Request, Response } from 'express';
|
|
7
|
+
import { securityConfig } from '../config';
|
|
8
|
+
|
|
9
|
+
// ────────────────────────────────────────────────────────────
|
|
10
|
+
// STANDARD API RATE LIMITER
|
|
11
|
+
// ────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export const apiRateLimit = rateLimit({
|
|
14
|
+
windowMs: securityConfig.rateLimit.windowMs,
|
|
15
|
+
max: securityConfig.rateLimit.maxRequests,
|
|
16
|
+
standardHeaders: true,
|
|
17
|
+
legacyHeaders: false,
|
|
18
|
+
skipFailedRequests: securityConfig.rateLimit.skipFailedRequests,
|
|
19
|
+
message: {
|
|
20
|
+
success: false,
|
|
21
|
+
error: {
|
|
22
|
+
code: 'RATE_LIMIT_EXCEEDED',
|
|
23
|
+
message: `Too many requests. Limit: ${securityConfig.rateLimit.maxRequests} per ${securityConfig.rateLimit.windowMs / 60000} minutes`,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// ────────────────────────────────────────────────────────────
|
|
29
|
+
// STRICT LIMITER (for auth endpoints)
|
|
30
|
+
// ────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export const authRateLimit = rateLimit({
|
|
33
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
34
|
+
max: 10,
|
|
35
|
+
standardHeaders: true,
|
|
36
|
+
legacyHeaders: false,
|
|
37
|
+
message: {
|
|
38
|
+
success: false,
|
|
39
|
+
error: {
|
|
40
|
+
code: 'AUTH_RATE_LIMIT_EXCEEDED',
|
|
41
|
+
message: 'Too many authentication attempts. Try again in 15 minutes.',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
skipSuccessfulRequests: true,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ────────────────────────────────────────────────────────────
|
|
48
|
+
// WEBHOOK LIMITER
|
|
49
|
+
// ────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export const webhookRateLimit = rateLimit({
|
|
52
|
+
windowMs: 60 * 1000, // 1 minute
|
|
53
|
+
max: 500, // webhooks can be high-volume
|
|
54
|
+
standardHeaders: true,
|
|
55
|
+
legacyHeaders: false,
|
|
56
|
+
message: {
|
|
57
|
+
success: false,
|
|
58
|
+
error: { code: 'WEBHOOK_RATE_LIMIT_EXCEEDED', message: 'Webhook rate limit exceeded' },
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ────────────────────────────────────────────────────────────
|
|
63
|
+
// EVIDENCE SUBMISSION LIMITER
|
|
64
|
+
// ────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export const evidenceRateLimit = rateLimit({
|
|
67
|
+
windowMs: 60 * 60 * 1000, // 1 hour
|
|
68
|
+
max: 200,
|
|
69
|
+
standardHeaders: true,
|
|
70
|
+
legacyHeaders: false,
|
|
71
|
+
keyGenerator: (req: Request) =>
|
|
72
|
+
(req as Request & { merchantId?: string }).merchantId ?? req.ip ?? 'unknown',
|
|
73
|
+
message: {
|
|
74
|
+
success: false,
|
|
75
|
+
error: { code: 'EVIDENCE_RATE_LIMIT_EXCEEDED', message: 'Evidence submission rate limit exceeded' },
|
|
76
|
+
},
|
|
77
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// CHARGEBACK GUARD — Request Validation Schemas (Zod)
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { Request, Response, NextFunction } from 'express';
|
|
7
|
+
import { createLogger } from '../utils/logger';
|
|
8
|
+
|
|
9
|
+
const log = createLogger('Validation');
|
|
10
|
+
|
|
11
|
+
// ────────────────────────────────────────────────────────────
|
|
12
|
+
// SCHEMAS
|
|
13
|
+
// ────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export const RegisterPaymentSchema = z.object({
|
|
16
|
+
orderId: z.string().min(1).max(255),
|
|
17
|
+
amount: z.number().positive(),
|
|
18
|
+
currency: z.string().length(3).toUpperCase(),
|
|
19
|
+
customerEmail: z.string().email(),
|
|
20
|
+
customerName: z.string().max(255).optional(),
|
|
21
|
+
customerIp: z.string().ip().optional(),
|
|
22
|
+
userAgent: z.string().max(512).optional(),
|
|
23
|
+
transactionId: z.string().max(255).optional(),
|
|
24
|
+
paymentMethod: z.string().max(50).optional(),
|
|
25
|
+
cardLastFour: z.string().max(4).optional(),
|
|
26
|
+
cardBrand: z.string().max(50).optional(),
|
|
27
|
+
provider: z.enum(['stripe', 'paypal', 'square', 'braintree']).optional(),
|
|
28
|
+
shippingAddress: z.object({
|
|
29
|
+
line1: z.string().max(255),
|
|
30
|
+
line2: z.string().max(255).optional(),
|
|
31
|
+
city: z.string().max(100),
|
|
32
|
+
state: z.string().max(100).optional(),
|
|
33
|
+
postalCode: z.string().max(20),
|
|
34
|
+
country: z.string().length(2).toUpperCase(),
|
|
35
|
+
}).optional(),
|
|
36
|
+
sessionData: z.object({
|
|
37
|
+
sessionId: z.string().optional(),
|
|
38
|
+
duration: z.number().min(0).optional(),
|
|
39
|
+
pageViews: z.array(z.any()).optional(),
|
|
40
|
+
clickTracking: z.array(z.any()).optional(),
|
|
41
|
+
timeOnCheckout: z.number().min(0).optional(),
|
|
42
|
+
referrer: z.string().url().optional(),
|
|
43
|
+
}).optional(),
|
|
44
|
+
deviceFingerprint: z.object({
|
|
45
|
+
screenResolution: z.string().optional(),
|
|
46
|
+
timezone: z.string().optional(),
|
|
47
|
+
language: z.string().optional(),
|
|
48
|
+
platform: z.string().optional(),
|
|
49
|
+
}).optional(),
|
|
50
|
+
metadata: z.record(z.unknown()).optional(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const DisputeHandleSchema = z.object({
|
|
54
|
+
id: z.string().min(1),
|
|
55
|
+
orderId: z.string().optional(),
|
|
56
|
+
amount: z.number().min(0),
|
|
57
|
+
currency: z.string().optional(),
|
|
58
|
+
reason: z.string().min(1),
|
|
59
|
+
status: z.string().min(1),
|
|
60
|
+
provider: z.enum(['stripe', 'paypal', 'square', 'braintree']),
|
|
61
|
+
evidenceDueBy: z.string().optional(),
|
|
62
|
+
metadata: z.record(z.unknown()).optional(),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const PaginationSchema = z.object({
|
|
66
|
+
page: z.coerce.number().int().positive().default(1),
|
|
67
|
+
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
68
|
+
sortBy: z.string().optional(),
|
|
69
|
+
sortOrder: z.enum(['asc', 'desc']).default('desc'),
|
|
70
|
+
from: z.string().optional(),
|
|
71
|
+
to: z.string().optional(),
|
|
72
|
+
status: z.string().optional(),
|
|
73
|
+
reason: z.string().optional(),
|
|
74
|
+
provider: z.string().optional(),
|
|
75
|
+
search: z.string().max(255).optional(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const MerchantRegisterSchema = z.object({
|
|
79
|
+
name: z.string().min(2).max(255),
|
|
80
|
+
email: z.string().email(),
|
|
81
|
+
password: z.string().min(8).max(128),
|
|
82
|
+
webhookUrl: z.string().url().optional(),
|
|
83
|
+
plan: z.enum(['free', 'starter', 'pro', 'enterprise']).default('free'),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export const MerchantLoginSchema = z.object({
|
|
87
|
+
email: z.string().email(),
|
|
88
|
+
password: z.string().min(1),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const UpdateShippingSchema = z.object({
|
|
92
|
+
orderId: z.string().min(1),
|
|
93
|
+
trackingNumber: z.string().min(1),
|
|
94
|
+
carrier: z.string().optional(),
|
|
95
|
+
shippedAt: z.string().optional(),
|
|
96
|
+
estimatedDelivery: z.string().optional(),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ────────────────────────────────────────────────────────────
|
|
100
|
+
// VALIDATION MIDDLEWARE FACTORY
|
|
101
|
+
// ────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
type SchemaSource = 'body' | 'query' | 'params';
|
|
104
|
+
|
|
105
|
+
export function validate<T extends z.ZodTypeAny>(
|
|
106
|
+
schema: T,
|
|
107
|
+
source: SchemaSource = 'body'
|
|
108
|
+
): (req: Request, res: Response, next: NextFunction) => void {
|
|
109
|
+
return (req: Request, res: Response, next: NextFunction): void => {
|
|
110
|
+
const result = schema.safeParse(req[source]);
|
|
111
|
+
|
|
112
|
+
if (!result.success) {
|
|
113
|
+
const errors = result.error.errors.map(e => ({
|
|
114
|
+
field: e.path.join('.'),
|
|
115
|
+
message: e.message,
|
|
116
|
+
code: e.code,
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
log.debug('Validation failed', { source, errors });
|
|
120
|
+
|
|
121
|
+
res.status(422).json({
|
|
122
|
+
success: false,
|
|
123
|
+
error: {
|
|
124
|
+
code: 'VALIDATION_ERROR',
|
|
125
|
+
message: 'Request validation failed',
|
|
126
|
+
details: { errors },
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Attach parsed & coerced data
|
|
133
|
+
if (source === 'body') { req.body = result.data; }
|
|
134
|
+
else if (source === 'query') { req.query = result.data as typeof req.query; }
|
|
135
|
+
|
|
136
|
+
next();
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ────────────────────────────────────────────────────────────
|
|
141
|
+
// SANITIZE MIDDLEWARE (removes XSS risk from strings)
|
|
142
|
+
// ────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
export function sanitizeBody(req: Request, _res: Response, next: NextFunction): void {
|
|
145
|
+
if (req.body && typeof req.body === 'object') {
|
|
146
|
+
req.body = sanitizeObject(req.body as Record<string, unknown>);
|
|
147
|
+
}
|
|
148
|
+
next();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function sanitizeObject(obj: Record<string, unknown>): Record<string, unknown> {
|
|
152
|
+
const result: Record<string, unknown> = {};
|
|
153
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
154
|
+
if (typeof value === 'string') {
|
|
155
|
+
result[key] = value
|
|
156
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
157
|
+
.replace(/on\w+="[^"]*"/gi, '')
|
|
158
|
+
.trim();
|
|
159
|
+
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
160
|
+
result[key] = sanitizeObject(value as Record<string, unknown>);
|
|
161
|
+
} else {
|
|
162
|
+
result[key] = value;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// CHARGEBACK GUARD — HTTP Server Entry Point
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import dotenv from 'dotenv';
|
|
6
|
+
dotenv.config();
|
|
7
|
+
|
|
8
|
+
import http from 'http';
|
|
9
|
+
import { createChargebackGuard } from './index';
|
|
10
|
+
import { WebhookHandler } from './integrations/webhook';
|
|
11
|
+
import { createApp } from './api/routes';
|
|
12
|
+
import { createLogger } from './utils/logger';
|
|
13
|
+
import { appConfig } from './config';
|
|
14
|
+
import { DisputeStatus, NotificationEvent } from './types';
|
|
15
|
+
|
|
16
|
+
const log = createLogger('Server');
|
|
17
|
+
|
|
18
|
+
async function bootstrap(): Promise<void> {
|
|
19
|
+
// ── 1. Initialize ChargebackGuard ────────────────────────
|
|
20
|
+
log.info('Bootstrapping ChargebackGuard...');
|
|
21
|
+
|
|
22
|
+
const guard = await createChargebackGuard({
|
|
23
|
+
autoReply: true,
|
|
24
|
+
evidenceCollection: true,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ── 2. Setup Webhook Handler ─────────────────────────────
|
|
28
|
+
const webhookHandler = new WebhookHandler();
|
|
29
|
+
|
|
30
|
+
webhookHandler.onDisputeDetected(async (dispute) => {
|
|
31
|
+
log.info(`Processing dispute: ${dispute.id}`);
|
|
32
|
+
try {
|
|
33
|
+
await guard.handleDispute(dispute);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
log.error('Dispute handling error', {
|
|
36
|
+
error: err instanceof Error ? err.message : String(err),
|
|
37
|
+
disputeId: dispute.id,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
webhookHandler.onDisputeResolved(async (disputeId, status, provider) => {
|
|
43
|
+
const event = status === DisputeStatus.WON
|
|
44
|
+
? NotificationEvent.DISPUTE_WON
|
|
45
|
+
: NotificationEvent.DISPUTE_LOST;
|
|
46
|
+
|
|
47
|
+
guard.events.emitEvent(event, { disputeId, status, provider });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ── 3. Create Express app ────────────────────────────────
|
|
51
|
+
const app = createApp(webhookHandler);
|
|
52
|
+
app.locals['chargebackGuard'] = guard;
|
|
53
|
+
|
|
54
|
+
// ── 4. Start HTTP server ─────────────────────────────────
|
|
55
|
+
const server = http.createServer(app);
|
|
56
|
+
|
|
57
|
+
server.listen(appConfig.port, () => {
|
|
58
|
+
log.info(`🛡️ ChargebackGuard API running`, {
|
|
59
|
+
url: `http://localhost:${appConfig.port}`,
|
|
60
|
+
environment: appConfig.env,
|
|
61
|
+
version: appConfig.version,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
console.log(`
|
|
65
|
+
╔══════════════════════════════════════════════════╗
|
|
66
|
+
║ 🛡️ CHARGEBACK GUARD v${appConfig.version} ║
|
|
67
|
+
╠══════════════════════════════════════════════════╣
|
|
68
|
+
║ Server: http://localhost:${appConfig.port} ║
|
|
69
|
+
║ Env: ${appConfig.env.padEnd(38)} ║
|
|
70
|
+
╠══════════════════════════════════════════════════╣
|
|
71
|
+
║ API ENDPOINTS ║
|
|
72
|
+
║ POST /api/v1/auth/register — Register merchant ║
|
|
73
|
+
║ POST /api/v1/auth/login — Login ║
|
|
74
|
+
║ POST /api/v1/payments — Register payment ║
|
|
75
|
+
║ GET /api/v1/disputes — List disputes ║
|
|
76
|
+
║ POST /api/v1/webhooks/stripe — Stripe webhook ║
|
|
77
|
+
║ POST /api/v1/webhooks/paypal — PayPal webhook ║
|
|
78
|
+
║ GET /api/v1/analytics/stats — Stats ║
|
|
79
|
+
║ GET /api/v1/health — Health check ║
|
|
80
|
+
╚══════════════════════════════════════════════════╝
|
|
81
|
+
`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ── 5. Graceful shutdown ─────────────────────────────────
|
|
85
|
+
const shutdown = async (signal: string): Promise<void> => {
|
|
86
|
+
log.info(`Received ${signal} — shutting down gracefully`);
|
|
87
|
+
|
|
88
|
+
server.close(async () => {
|
|
89
|
+
try {
|
|
90
|
+
await guard.shutdown();
|
|
91
|
+
log.info('Shutdown complete');
|
|
92
|
+
process.exit(0);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
log.error('Shutdown error', { error: err instanceof Error ? err.message : String(err) });
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Force shutdown after 30s
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
log.error('Forced shutdown after timeout');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}, 30000);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
107
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
108
|
+
|
|
109
|
+
process.on('uncaughtException', (err) => {
|
|
110
|
+
log.error('Uncaught exception', { error: err.message, stack: err.stack });
|
|
111
|
+
shutdown('uncaughtException').catch(console.error);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
process.on('unhandledRejection', (reason) => {
|
|
115
|
+
log.error('Unhandled promise rejection', { reason: String(reason) });
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
bootstrap().catch(err => {
|
|
120
|
+
console.error('Bootstrap failed:', err);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
});
|