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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +311 -0
  3. package/docs/api.md +278 -0
  4. package/docs/architecture.md +281 -0
  5. package/docs/configuration.md +292 -0
  6. package/docs/getting-started.md +155 -0
  7. package/examples/advancedConfig.ts +123 -0
  8. package/examples/basicUsage.ts +98 -0
  9. package/examples/stripeIntegration.ts +106 -0
  10. package/package.json +181 -0
  11. package/src/ai/fraudDetection.ts +261 -0
  12. package/src/ai/patternRecognition.ts +218 -0
  13. package/src/analytics/dashboard.ts +195 -0
  14. package/src/analytics/metrics.ts +175 -0
  15. package/src/analytics/predictions.ts +135 -0
  16. package/src/analytics/reports.ts +221 -0
  17. package/src/api/controllers.ts +339 -0
  18. package/src/api/middleware.ts +172 -0
  19. package/src/api/routes.ts +141 -0
  20. package/src/config.ts +231 -0
  21. package/src/core/chargebackGuard.ts +616 -0
  22. package/src/core/eventEmitter.ts +118 -0
  23. package/src/core/lifecycle.ts +215 -0
  24. package/src/database/schema.ts +392 -0
  25. package/src/dispute/analyzer.ts +317 -0
  26. package/src/dispute/bankIntegration.ts +274 -0
  27. package/src/dispute/detector.ts +239 -0
  28. package/src/dispute/responseEngine.ts +440 -0
  29. package/src/evidence/collector.ts +426 -0
  30. package/src/evidence/encryption.ts +168 -0
  31. package/src/evidence/storage.ts +197 -0
  32. package/src/evidence/validator.ts +184 -0
  33. package/src/index.ts +43 -0
  34. package/src/integrations/paypal.ts +258 -0
  35. package/src/integrations/stripe.ts +280 -0
  36. package/src/integrations/webhook.ts +332 -0
  37. package/src/notifications/email.ts +161 -0
  38. package/src/notifications/inApp.ts +319 -0
  39. package/src/notifications/sms.ts +58 -0
  40. package/src/security/auth.ts +153 -0
  41. package/src/security/rateLimit.ts +77 -0
  42. package/src/security/validation.ts +166 -0
  43. package/src/server.ts +122 -0
  44. package/src/types/index.ts +790 -0
  45. package/src/utils/formatters.ts +72 -0
  46. package/src/utils/helpers.ts +193 -0
  47. package/src/utils/logger.ts +88 -0
  48. 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
+ }