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,339 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// CHARGEBACK GUARD — API Controllers
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import { Request, Response } from 'express';
|
|
6
|
+
import { createLogger } from '../utils/logger';
|
|
7
|
+
import { appConfig } from '../config';
|
|
8
|
+
import {
|
|
9
|
+
ApiResponse,
|
|
10
|
+
ApiMeta,
|
|
11
|
+
PaginationMeta,
|
|
12
|
+
FilterQuery,
|
|
13
|
+
} from '../types';
|
|
14
|
+
import { AuthenticatedRequest } from '../security/auth';
|
|
15
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
16
|
+
|
|
17
|
+
const log = createLogger('Controllers');
|
|
18
|
+
|
|
19
|
+
// ────────────────────────────────────────────────────────────
|
|
20
|
+
// RESPONSE HELPERS
|
|
21
|
+
// ────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
function makeMeta(req: Request, pagination?: PaginationMeta): ApiMeta {
|
|
24
|
+
return {
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
requestId: (req as Request & { requestId?: string }).requestId ?? uuidv4(),
|
|
27
|
+
version: appConfig.version,
|
|
28
|
+
pagination,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ok<T>(res: Response, req: Request, data: T, pagination?: PaginationMeta): void {
|
|
33
|
+
const body: ApiResponse<T> = {
|
|
34
|
+
success: true,
|
|
35
|
+
data,
|
|
36
|
+
meta: makeMeta(req, pagination),
|
|
37
|
+
};
|
|
38
|
+
res.status(200).json(body);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function created<T>(res: Response, req: Request, data: T): void {
|
|
42
|
+
const body: ApiResponse<T> = {
|
|
43
|
+
success: true,
|
|
44
|
+
data,
|
|
45
|
+
meta: makeMeta(req),
|
|
46
|
+
};
|
|
47
|
+
res.status(201).json(body);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function notFound(res: Response, req: Request, message: string): void {
|
|
51
|
+
res.status(404).json({
|
|
52
|
+
success: false,
|
|
53
|
+
error: { code: 'NOT_FOUND', message },
|
|
54
|
+
meta: makeMeta(req),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function badRequest(res: Response, req: Request, message: string, details?: unknown): void {
|
|
59
|
+
res.status(400).json({
|
|
60
|
+
success: false,
|
|
61
|
+
error: { code: 'BAD_REQUEST', message, details },
|
|
62
|
+
meta: makeMeta(req),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ────────────────────────────────────────────────────────────
|
|
67
|
+
// PAYMENTS CONTROLLER
|
|
68
|
+
// ────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
export const PaymentsController = {
|
|
71
|
+
|
|
72
|
+
async register(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
73
|
+
try {
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
75
|
+
const { ChargebackGuard } = require('../core/chargebackGuard');
|
|
76
|
+
const guard = req.app.locals['chargebackGuard'] as InstanceType<typeof ChargebackGuard>;
|
|
77
|
+
|
|
78
|
+
const result = await guard.registerPayment({
|
|
79
|
+
...req.body,
|
|
80
|
+
customerIp: req.body.customerIp ?? req.ip,
|
|
81
|
+
userAgent: req.body.userAgent ?? req.headers['user-agent'],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
created(res, req, result);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const message = err instanceof Error ? err.message : 'Registration failed';
|
|
87
|
+
log.error('Payment registration error', { error: message });
|
|
88
|
+
badRequest(res, req, message);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
async list(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
93
|
+
try {
|
|
94
|
+
const query = req.query as FilterQuery;
|
|
95
|
+
const page = Number(query.page ?? 1);
|
|
96
|
+
const limit = Number(query.limit ?? 20);
|
|
97
|
+
|
|
98
|
+
// Placeholder — DB query goes here
|
|
99
|
+
const items: unknown[] = [];
|
|
100
|
+
const total = 0;
|
|
101
|
+
|
|
102
|
+
ok(res, req, items, {
|
|
103
|
+
page, limit, total,
|
|
104
|
+
totalPages: Math.ceil(total / limit),
|
|
105
|
+
hasNext: page * limit < total,
|
|
106
|
+
hasPrev: page > 1,
|
|
107
|
+
});
|
|
108
|
+
} catch (err) {
|
|
109
|
+
log.error('List payments error', { error: err instanceof Error ? err.message : err });
|
|
110
|
+
res.status(500).json({ success: false, error: { code: 'INTERNAL_ERROR', message: 'Failed to list payments' } });
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
async updateShipping(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
115
|
+
try {
|
|
116
|
+
const { orderId } = req.params as { orderId: string };
|
|
117
|
+
const { trackingNumber, carrier, shippedAt, estimatedDelivery } = req.body as {
|
|
118
|
+
trackingNumber: string;
|
|
119
|
+
carrier?: string;
|
|
120
|
+
shippedAt?: string;
|
|
121
|
+
estimatedDelivery?: string;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
125
|
+
const { EvidenceStorage } = require('../evidence/storage');
|
|
126
|
+
const storage = new EvidenceStorage();
|
|
127
|
+
|
|
128
|
+
const updated = await storage.update(orderId, {
|
|
129
|
+
data: {
|
|
130
|
+
shippingData: { trackingNumber, carrier, shippingDate: shippedAt, estimatedDelivery },
|
|
131
|
+
},
|
|
132
|
+
} as Parameters<typeof storage.update>[1]);
|
|
133
|
+
|
|
134
|
+
if (!updated) {
|
|
135
|
+
notFound(res, req, `Order not found: ${orderId}`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
ok(res, req, { orderId, trackingNumber, updated: true });
|
|
140
|
+
} catch (err) {
|
|
141
|
+
log.error('Update shipping error', { error: err instanceof Error ? err.message : err });
|
|
142
|
+
badRequest(res, req, err instanceof Error ? err.message : 'Update failed');
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// ────────────────────────────────────────────────────────────
|
|
148
|
+
// DISPUTES CONTROLLER
|
|
149
|
+
// ────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
export const DisputesController = {
|
|
152
|
+
|
|
153
|
+
async list(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
154
|
+
try {
|
|
155
|
+
const { StripeIntegration } = require('../integrations/stripe');
|
|
156
|
+
const stripe = new StripeIntegration();
|
|
157
|
+
const disputes = await stripe.listActionableDisputes();
|
|
158
|
+
ok(res, req, disputes);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
log.error('List disputes error', { error: err instanceof Error ? err.message : err });
|
|
161
|
+
res.status(500).json({ success: false, error: { code: 'INTERNAL_ERROR', message: 'Failed to list disputes' } });
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
async getOne(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
166
|
+
try {
|
|
167
|
+
const { id } = req.params as { id: string };
|
|
168
|
+
const { StripeIntegration } = require('../integrations/stripe');
|
|
169
|
+
const stripe = new StripeIntegration();
|
|
170
|
+
const dispute = await stripe.getDispute(id);
|
|
171
|
+
ok(res, req, dispute);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
notFound(res, req, `Dispute not found: ${req.params['id']}`);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
async handle(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
178
|
+
try {
|
|
179
|
+
const { ChargebackGuard } = require('../core/chargebackGuard');
|
|
180
|
+
const guard = req.app.locals['chargebackGuard'] as InstanceType<typeof ChargebackGuard>;
|
|
181
|
+
const result = await guard.handleDispute(req.body);
|
|
182
|
+
ok(res, req, result);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
const message = err instanceof Error ? err.message : 'Dispute handling failed';
|
|
185
|
+
log.error('Handle dispute error', { error: message });
|
|
186
|
+
res.status(500).json({ success: false, error: { code: 'INTERNAL_ERROR', message } });
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
async submitReply(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
191
|
+
try {
|
|
192
|
+
const { id } = req.params as { id: string };
|
|
193
|
+
const reply = req.body;
|
|
194
|
+
|
|
195
|
+
const { StripeIntegration } = require('../integrations/stripe');
|
|
196
|
+
const stripe = new StripeIntegration();
|
|
197
|
+
await stripe.submitDisputeEvidence(id, reply);
|
|
198
|
+
|
|
199
|
+
ok(res, req, { disputeId: id, submitted: true, submittedAt: new Date().toISOString() });
|
|
200
|
+
} catch (err) {
|
|
201
|
+
const message = err instanceof Error ? err.message : 'Submission failed';
|
|
202
|
+
badRequest(res, req, message);
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// ────────────────────────────────────────────────────────────
|
|
208
|
+
// ANALYTICS CONTROLLER
|
|
209
|
+
// ────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
export const AnalyticsController = {
|
|
212
|
+
|
|
213
|
+
async getStats(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
214
|
+
try {
|
|
215
|
+
const { ChargebackGuard } = require('../core/chargebackGuard');
|
|
216
|
+
const guard = req.app.locals['chargebackGuard'] as InstanceType<typeof ChargebackGuard>;
|
|
217
|
+
const q = req.query as { from?: string; to?: string };
|
|
218
|
+
const stats = await guard.getProtectionStats(
|
|
219
|
+
q.from ? new Date(q.from) : undefined,
|
|
220
|
+
q.to ? new Date(q.to) : undefined
|
|
221
|
+
);
|
|
222
|
+
ok(res, req, stats);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
log.error('Stats error', { error: err instanceof Error ? err.message : err });
|
|
225
|
+
res.status(500).json({ success: false, error: { code: 'INTERNAL_ERROR', message: 'Stats unavailable' } });
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
async getDashboard(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
230
|
+
try {
|
|
231
|
+
const { DashboardService } = require('../analytics/dashboard');
|
|
232
|
+
const svc = new DashboardService();
|
|
233
|
+
const dashboard = await svc.getMetrics(req.merchantId);
|
|
234
|
+
ok(res, req, dashboard);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
log.error('Dashboard error', { error: err instanceof Error ? err.message : err });
|
|
237
|
+
res.status(500).json({ success: false, error: { code: 'INTERNAL_ERROR', message: 'Dashboard unavailable' } });
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// ────────────────────────────────────────────────────────────
|
|
243
|
+
// HEALTH CONTROLLER
|
|
244
|
+
// ────────────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
export const HealthController = {
|
|
247
|
+
|
|
248
|
+
async check(req: Request, res: Response): Promise<void> {
|
|
249
|
+
try {
|
|
250
|
+
const guard = req.app.locals['chargebackGuard'];
|
|
251
|
+
const health = guard
|
|
252
|
+
? await guard.getHealth()
|
|
253
|
+
: { success: true, data: { status: 'healthy', uptime: process.uptime() * 1000 } };
|
|
254
|
+
|
|
255
|
+
const statusCode = health.success ? 200 : 503;
|
|
256
|
+
res.status(statusCode).json({
|
|
257
|
+
...health,
|
|
258
|
+
meta: makeMeta(req),
|
|
259
|
+
});
|
|
260
|
+
} catch {
|
|
261
|
+
res.status(503).json({ success: false, error: { code: 'HEALTH_CHECK_FAILED', message: 'Health check failed' } });
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
async ping(_req: Request, res: Response): Promise<void> {
|
|
266
|
+
res.status(200).json({ pong: true, timestamp: new Date().toISOString() });
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// ────────────────────────────────────────────────────────────
|
|
271
|
+
// AUTH CONTROLLER
|
|
272
|
+
// ────────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
export const AuthController = {
|
|
275
|
+
|
|
276
|
+
async register(req: Request, res: Response): Promise<void> {
|
|
277
|
+
try {
|
|
278
|
+
const { hashPassword, signToken, generateMerchantApiKey } = require('./../../src/security/auth');
|
|
279
|
+
const { name, email, password, webhookUrl, plan } = req.body as {
|
|
280
|
+
name: string;
|
|
281
|
+
email: string;
|
|
282
|
+
password: string;
|
|
283
|
+
webhookUrl?: string;
|
|
284
|
+
plan: string;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const passwordHash = await hashPassword(password);
|
|
288
|
+
const merchantId = uuidv4();
|
|
289
|
+
const apiKey = generateMerchantApiKey();
|
|
290
|
+
|
|
291
|
+
// Store merchant (placeholder — DB layer goes here)
|
|
292
|
+
const merchant = {
|
|
293
|
+
merchantId,
|
|
294
|
+
name,
|
|
295
|
+
email,
|
|
296
|
+
passwordHash,
|
|
297
|
+
apiKey,
|
|
298
|
+
webhookUrl,
|
|
299
|
+
plan,
|
|
300
|
+
createdAt: new Date().toISOString(),
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const token = signToken({ merchantId, email, plan });
|
|
304
|
+
|
|
305
|
+
created(res, req, {
|
|
306
|
+
merchantId,
|
|
307
|
+
email,
|
|
308
|
+
name,
|
|
309
|
+
plan,
|
|
310
|
+
apiKey,
|
|
311
|
+
token,
|
|
312
|
+
});
|
|
313
|
+
} catch (err) {
|
|
314
|
+
const message = err instanceof Error ? err.message : 'Registration failed';
|
|
315
|
+
badRequest(res, req, message);
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
async login(req: Request, res: Response): Promise<void> {
|
|
320
|
+
try {
|
|
321
|
+
const { signToken } = require('./../../src/security/auth');
|
|
322
|
+
const { email, password } = req.body as { email: string; password: string };
|
|
323
|
+
|
|
324
|
+
// Placeholder — real implementation queries DB
|
|
325
|
+
log.info(`Login attempt: ${email}`);
|
|
326
|
+
|
|
327
|
+
// For demo: return a token
|
|
328
|
+
const token = signToken({
|
|
329
|
+
merchantId: uuidv4(),
|
|
330
|
+
email,
|
|
331
|
+
plan: 'pro',
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
ok(res, req, { token, email });
|
|
335
|
+
} catch (err) {
|
|
336
|
+
res.status(401).json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Invalid credentials' } });
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// CHARGEBACK GUARD — Express Middleware Stack
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import express, { Application, Request, Response, NextFunction } from 'express';
|
|
6
|
+
import cors from 'cors';
|
|
7
|
+
import helmet from 'helmet';
|
|
8
|
+
import compression from 'compression';
|
|
9
|
+
import morgan from 'morgan';
|
|
10
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
11
|
+
import { createLogger } from '../utils/logger';
|
|
12
|
+
import { corsConfig, appConfig } from '../config';
|
|
13
|
+
import { apiRateLimit } from '../security/rateLimit';
|
|
14
|
+
import { sanitizeBody } from '../security/validation';
|
|
15
|
+
|
|
16
|
+
const log = createLogger('Middleware');
|
|
17
|
+
|
|
18
|
+
// ────────────────────────────────────────────────────────────
|
|
19
|
+
// REQUEST ID MIDDLEWARE
|
|
20
|
+
// ────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export function requestId(req: Request, res: Response, next: NextFunction): void {
|
|
23
|
+
const id = (req.headers['x-request-id'] as string) ?? uuidv4();
|
|
24
|
+
(req as Request & { requestId: string }).requestId = id;
|
|
25
|
+
res.setHeader('X-Request-Id', id);
|
|
26
|
+
next();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ────────────────────────────────────────────────────────────
|
|
30
|
+
// RESPONSE TIME MIDDLEWARE
|
|
31
|
+
// ────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export function responseTime(req: Request, res: Response, next: NextFunction): void {
|
|
34
|
+
const start = Date.now();
|
|
35
|
+
res.on('finish', () => {
|
|
36
|
+
const duration = Date.now() - start;
|
|
37
|
+
res.setHeader('X-Response-Time', `${duration}ms`);
|
|
38
|
+
});
|
|
39
|
+
next();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ────────────────────────────────────────────────────────────
|
|
43
|
+
// RAW BODY CAPTURE (needed for Stripe webhook verification)
|
|
44
|
+
// ────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export function captureRawBody(req: Request, res: Response, next: NextFunction): void {
|
|
47
|
+
if (req.path.includes('/webhook')) {
|
|
48
|
+
let raw = '';
|
|
49
|
+
req.on('data', (chunk: Buffer) => { raw += chunk.toString(); });
|
|
50
|
+
req.on('end', () => {
|
|
51
|
+
(req as Request & { rawBody: string }).rawBody = raw;
|
|
52
|
+
next();
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
next();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ────────────────────────────────────────────────────────────
|
|
60
|
+
// NOT FOUND HANDLER
|
|
61
|
+
// ────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
export function notFoundHandler(req: Request, res: Response): void {
|
|
64
|
+
res.status(404).json({
|
|
65
|
+
success: false,
|
|
66
|
+
error: {
|
|
67
|
+
code: 'NOT_FOUND',
|
|
68
|
+
message: `Route not found: ${req.method} ${req.path}`,
|
|
69
|
+
},
|
|
70
|
+
meta: {
|
|
71
|
+
timestamp: new Date().toISOString(),
|
|
72
|
+
requestId: (req as Request & { requestId?: string }).requestId,
|
|
73
|
+
version: appConfig.version,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ────────────────────────────────────────────────────────────
|
|
79
|
+
// GLOBAL ERROR HANDLER
|
|
80
|
+
// ────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export function errorHandler(
|
|
83
|
+
err: Error & { status?: number; code?: string },
|
|
84
|
+
req: Request,
|
|
85
|
+
res: Response,
|
|
86
|
+
_next: NextFunction
|
|
87
|
+
): void {
|
|
88
|
+
const status = err.status ?? 500;
|
|
89
|
+
const code = err.code ?? 'INTERNAL_ERROR';
|
|
90
|
+
|
|
91
|
+
log.error('Unhandled error', {
|
|
92
|
+
status,
|
|
93
|
+
code,
|
|
94
|
+
message: err.message,
|
|
95
|
+
path: req.path,
|
|
96
|
+
method: req.method,
|
|
97
|
+
requestId: (req as Request & { requestId?: string }).requestId,
|
|
98
|
+
stack: appConfig.isDev ? err.stack : undefined,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
res.status(status).json({
|
|
102
|
+
success: false,
|
|
103
|
+
error: {
|
|
104
|
+
code,
|
|
105
|
+
message: status === 500 ? 'Internal server error' : err.message,
|
|
106
|
+
...(appConfig.isDev && { stack: err.stack }),
|
|
107
|
+
},
|
|
108
|
+
meta: {
|
|
109
|
+
timestamp: new Date().toISOString(),
|
|
110
|
+
requestId: (req as Request & { requestId?: string }).requestId,
|
|
111
|
+
version: appConfig.version,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ────────────────────────────────────────────────────────────
|
|
117
|
+
// APPLY ALL MIDDLEWARE
|
|
118
|
+
// ────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
export function applyMiddleware(app: Application): void {
|
|
121
|
+
// Security headers
|
|
122
|
+
app.use(helmet({
|
|
123
|
+
crossOriginResourcePolicy: { policy: 'same-site' },
|
|
124
|
+
contentSecurityPolicy: {
|
|
125
|
+
directives: {
|
|
126
|
+
defaultSrc: ["'self'"],
|
|
127
|
+
scriptSrc: ["'self'"],
|
|
128
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
129
|
+
imgSrc: ["'self'", 'data:'],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
// CORS
|
|
135
|
+
app.use(cors({
|
|
136
|
+
origin: corsConfig.origins,
|
|
137
|
+
methods: corsConfig.methods,
|
|
138
|
+
credentials: true,
|
|
139
|
+
optionsSuccessStatus: 200,
|
|
140
|
+
}));
|
|
141
|
+
|
|
142
|
+
// Compression
|
|
143
|
+
app.use(compression());
|
|
144
|
+
|
|
145
|
+
// Request ID
|
|
146
|
+
app.use(requestId);
|
|
147
|
+
app.use(responseTime);
|
|
148
|
+
|
|
149
|
+
// Raw body capture (before JSON parsing for webhooks)
|
|
150
|
+
app.use(captureRawBody);
|
|
151
|
+
|
|
152
|
+
// Body parsers
|
|
153
|
+
app.use(express.json({ limit: '5mb' }));
|
|
154
|
+
app.use(express.urlencoded({ extended: true, limit: '5mb' }));
|
|
155
|
+
|
|
156
|
+
// Sanitize inputs
|
|
157
|
+
app.use(sanitizeBody);
|
|
158
|
+
|
|
159
|
+
// Request logging
|
|
160
|
+
if (appConfig.isDev) {
|
|
161
|
+
app.use(morgan('dev'));
|
|
162
|
+
} else {
|
|
163
|
+
app.use(morgan('combined', {
|
|
164
|
+
stream: { write: (msg: string) => log.info(msg.trim()) },
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Global rate limiting
|
|
169
|
+
app.use('/api/', apiRateLimit);
|
|
170
|
+
|
|
171
|
+
log.debug('Middleware stack applied');
|
|
172
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// CHARGEBACK GUARD — API Routes (Express Router)
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import { Router } from 'express';
|
|
6
|
+
import {
|
|
7
|
+
PaymentsController,
|
|
8
|
+
DisputesController,
|
|
9
|
+
AnalyticsController,
|
|
10
|
+
HealthController,
|
|
11
|
+
AuthController,
|
|
12
|
+
} from './controllers';
|
|
13
|
+
import { requireAuth } from '../security/auth';
|
|
14
|
+
import {
|
|
15
|
+
authRateLimit,
|
|
16
|
+
webhookRateLimit,
|
|
17
|
+
evidenceRateLimit,
|
|
18
|
+
} from '../security/rateLimit';
|
|
19
|
+
import {
|
|
20
|
+
validate,
|
|
21
|
+
RegisterPaymentSchema,
|
|
22
|
+
DisputeHandleSchema,
|
|
23
|
+
MerchantRegisterSchema,
|
|
24
|
+
MerchantLoginSchema,
|
|
25
|
+
PaginationSchema,
|
|
26
|
+
UpdateShippingSchema,
|
|
27
|
+
} from '../security/validation';
|
|
28
|
+
import { WebhookHandler } from '../integrations/webhook';
|
|
29
|
+
import { createLogger } from '../utils/logger';
|
|
30
|
+
|
|
31
|
+
const log = createLogger('Routes');
|
|
32
|
+
|
|
33
|
+
// ────────────────────────────────────────────────────────────
|
|
34
|
+
// ROOT ROUTER
|
|
35
|
+
// ────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
export function createRouter(webhookHandler: WebhookHandler): Router {
|
|
38
|
+
const router = Router();
|
|
39
|
+
|
|
40
|
+
// ── Health ──────────────────────────────
|
|
41
|
+
router.get('/ping', HealthController.ping);
|
|
42
|
+
router.get('/health', HealthController.check);
|
|
43
|
+
|
|
44
|
+
// ── Auth ────────────────────────────────
|
|
45
|
+
router.post('/auth/register',
|
|
46
|
+
authRateLimit,
|
|
47
|
+
validate(MerchantRegisterSchema),
|
|
48
|
+
AuthController.register
|
|
49
|
+
);
|
|
50
|
+
router.post('/auth/login',
|
|
51
|
+
authRateLimit,
|
|
52
|
+
validate(MerchantLoginSchema),
|
|
53
|
+
AuthController.login
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// ── Payments ────────────────────────────
|
|
57
|
+
router.post('/payments',
|
|
58
|
+
requireAuth,
|
|
59
|
+
evidenceRateLimit,
|
|
60
|
+
validate(RegisterPaymentSchema),
|
|
61
|
+
PaymentsController.register
|
|
62
|
+
);
|
|
63
|
+
router.get('/payments',
|
|
64
|
+
requireAuth,
|
|
65
|
+
validate(PaginationSchema, 'query'),
|
|
66
|
+
PaymentsController.list
|
|
67
|
+
);
|
|
68
|
+
router.patch('/payments/:orderId/shipping',
|
|
69
|
+
requireAuth,
|
|
70
|
+
validate(UpdateShippingSchema),
|
|
71
|
+
PaymentsController.updateShipping
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// ── Disputes ────────────────────────────
|
|
75
|
+
router.get('/disputes',
|
|
76
|
+
requireAuth,
|
|
77
|
+
validate(PaginationSchema, 'query'),
|
|
78
|
+
DisputesController.list
|
|
79
|
+
);
|
|
80
|
+
router.get('/disputes/:id',
|
|
81
|
+
requireAuth,
|
|
82
|
+
DisputesController.getOne
|
|
83
|
+
);
|
|
84
|
+
router.post('/disputes/handle',
|
|
85
|
+
requireAuth,
|
|
86
|
+
validate(DisputeHandleSchema),
|
|
87
|
+
DisputesController.handle
|
|
88
|
+
);
|
|
89
|
+
router.post('/disputes/:id/reply',
|
|
90
|
+
requireAuth,
|
|
91
|
+
DisputesController.submitReply
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// ── Analytics ───────────────────────────
|
|
95
|
+
router.get('/analytics/stats',
|
|
96
|
+
requireAuth,
|
|
97
|
+
AnalyticsController.getStats
|
|
98
|
+
);
|
|
99
|
+
router.get('/analytics/dashboard',
|
|
100
|
+
requireAuth,
|
|
101
|
+
AnalyticsController.getDashboard
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// ── Webhooks ────────────────────────────
|
|
105
|
+
router.post('/webhooks/stripe',
|
|
106
|
+
webhookRateLimit,
|
|
107
|
+
webhookHandler.handleStripe()
|
|
108
|
+
);
|
|
109
|
+
router.post('/webhooks/paypal',
|
|
110
|
+
webhookRateLimit,
|
|
111
|
+
webhookHandler.handlePayPal()
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
log.debug('Routes registered');
|
|
115
|
+
return router;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ────────────────────────────────────────────────────────────
|
|
119
|
+
// APP FACTORY
|
|
120
|
+
// ────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
import express, { Application } from 'express';
|
|
123
|
+
import { applyMiddleware, notFoundHandler, errorHandler } from './middleware';
|
|
124
|
+
|
|
125
|
+
export function createApp(webhookHandler: WebhookHandler): Application {
|
|
126
|
+
const app = express();
|
|
127
|
+
|
|
128
|
+
// Apply middleware stack
|
|
129
|
+
applyMiddleware(app);
|
|
130
|
+
|
|
131
|
+
// Mount API routes
|
|
132
|
+
app.use('/api/v1', createRouter(webhookHandler));
|
|
133
|
+
|
|
134
|
+
// 404 handler
|
|
135
|
+
app.use(notFoundHandler);
|
|
136
|
+
|
|
137
|
+
// Error handler (must be last)
|
|
138
|
+
app.use(errorHandler);
|
|
139
|
+
|
|
140
|
+
return app;
|
|
141
|
+
}
|