whale-code 6.5.7 → 6.5.9
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/README.md +14 -2
- package/dist/cli/services/agent-loop.js +26 -2
- package/dist/cli/services/agent-loop.js.map +1 -1
- package/dist/cli/services/hooks.js +2 -1
- package/dist/cli/services/hooks.js.map +1 -1
- package/dist/cli/services/telemetry-spans.js +1 -0
- package/dist/cli/services/telemetry-spans.js.map +1 -1
- package/dist/cli/services/telemetry.d.ts +23 -0
- package/dist/cli/services/telemetry.js +45 -1
- package/dist/cli/services/telemetry.js.map +1 -1
- package/dist/server/handlers/__test-utils__/test-db.d.ts +17 -3
- package/dist/server/handlers/__test-utils__/test-db.js +113 -14
- package/dist/server/handlers/__test-utils__/test-db.js.map +1 -1
- package/dist/server/handlers/affiliates.d.ts +9 -0
- package/dist/server/handlers/affiliates.js +197 -0
- package/dist/server/handlers/affiliates.js.map +1 -0
- package/dist/server/handlers/api-docs.d.ts +4 -2
- package/dist/server/handlers/api-docs.js +204 -1681
- package/dist/server/handlers/api-docs.js.map +1 -1
- package/dist/server/handlers/campaigns.d.ts +9 -0
- package/dist/server/handlers/campaigns.js +237 -0
- package/dist/server/handlers/campaigns.js.map +1 -0
- package/dist/server/handlers/catalog-schemas.js +9 -9
- package/dist/server/handlers/catalog-schemas.js.map +1 -1
- package/dist/server/handlers/catalog.js +1 -1
- package/dist/server/handlers/catalog.js.map +1 -1
- package/dist/server/handlers/comms-documents.js +28 -2
- package/dist/server/handlers/comms-documents.js.map +1 -1
- package/dist/server/handlers/comms-pdf-generation.js +25 -3
- package/dist/server/handlers/comms-pdf-generation.js.map +1 -1
- package/dist/server/handlers/comms-pdf-helpers.js +4 -4
- package/dist/server/handlers/comms-pdf-helpers.js.map +1 -1
- package/dist/server/handlers/comms.d.ts +100 -0
- package/dist/server/handlers/comms.js +146 -12
- package/dist/server/handlers/comms.js.map +1 -1
- package/dist/server/handlers/coupons.d.ts +9 -0
- package/dist/server/handlers/coupons.js +220 -0
- package/dist/server/handlers/coupons.js.map +1 -0
- package/dist/server/handlers/embeddings.js +1 -1
- package/dist/server/handlers/embeddings.js.map +1 -1
- package/dist/server/handlers/enrichment.js +2 -622
- package/dist/server/handlers/enrichment.js.map +1 -1
- package/dist/server/handlers/fulfillment.d.ts +9 -0
- package/dist/server/handlers/fulfillment.js +209 -0
- package/dist/server/handlers/fulfillment.js.map +1 -0
- package/dist/server/handlers/google-ads.d.ts +24 -0
- package/dist/server/handlers/google-ads.js +2199 -0
- package/dist/server/handlers/google-ads.js.map +1 -0
- package/dist/server/handlers/invoices.d.ts +9 -0
- package/dist/server/handlers/invoices.js +252 -0
- package/dist/server/handlers/invoices.js.map +1 -0
- package/dist/server/handlers/loyalty.d.ts +9 -0
- package/dist/server/handlers/loyalty.js +197 -0
- package/dist/server/handlers/loyalty.js.map +1 -0
- package/dist/server/handlers/meta-ads-graph-api.js +18 -3
- package/dist/server/handlers/meta-ads-graph-api.js.map +1 -1
- package/dist/server/handlers/phone.d.ts +9 -0
- package/dist/server/handlers/phone.js +197 -0
- package/dist/server/handlers/phone.js.map +1 -0
- package/dist/server/handlers/pipeline.d.ts +9 -0
- package/dist/server/handlers/pipeline.js +277 -0
- package/dist/server/handlers/pipeline.js.map +1 -0
- package/dist/server/handlers/qr-codes.d.ts +9 -0
- package/dist/server/handlers/qr-codes.js +198 -0
- package/dist/server/handlers/qr-codes.js.map +1 -0
- package/dist/server/handlers/reviews.d.ts +9 -0
- package/dist/server/handlers/reviews.js +171 -0
- package/dist/server/handlers/reviews.js.map +1 -0
- package/dist/server/handlers/segments.d.ts +9 -0
- package/dist/server/handlers/segments.js +229 -0
- package/dist/server/handlers/segments.js.map +1 -0
- package/dist/server/handlers/social.d.ts +9 -0
- package/dist/server/handlers/social.js +81 -0
- package/dist/server/handlers/social.js.map +1 -0
- package/dist/server/handlers/tax.d.ts +9 -0
- package/dist/server/handlers/tax.js +182 -0
- package/dist/server/handlers/tax.js.map +1 -0
- package/dist/server/handlers/wallet.d.ts +9 -0
- package/dist/server/handlers/wallet.js +203 -0
- package/dist/server/handlers/wallet.js.map +1 -0
- package/dist/server/handlers/webhooks-mgmt.d.ts +9 -0
- package/dist/server/handlers/webhooks-mgmt.js +181 -0
- package/dist/server/handlers/webhooks-mgmt.js.map +1 -0
- package/dist/server/handlers/wholesale.d.ts +9 -0
- package/dist/server/handlers/wholesale.js +219 -0
- package/dist/server/handlers/wholesale.js.map +1 -0
- package/dist/server/index.js +20 -9
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/clickhouse-buffer.js +1 -0
- package/dist/server/lib/clickhouse-buffer.js.map +1 -1
- package/dist/server/lib/coa-renderer.d.ts +1 -1
- package/dist/server/lib/coa-renderer.js +32 -10
- package/dist/server/lib/coa-renderer.js.map +1 -1
- package/dist/server/server-worker.d.ts +1 -0
- package/dist/server/server-worker.js +464 -3
- package/dist/server/server-worker.js.map +1 -1
- package/dist/server/tool-router.js +118 -4
- package/dist/server/tool-router.js.map +1 -1
- package/package.json +28 -4
- package/vendor/ink/package.json +0 -2
- package/whale-logo.png +0 -0
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
// server/handlers/enrichment.ts — Customer enrichment
|
|
2
|
-
// Integrates with PDL (Person Enrichment), Bright Data (LinkedIn), HIBP (Breach Check),
|
|
3
|
-
// DeHashed, and xonPlus (real-time breach monitoring).
|
|
1
|
+
// server/handlers/enrichment.ts — Customer enrichment via PDL and Bright Data LinkedIn.
|
|
4
2
|
// All external API keys are fetched via decrypt_secret RPC from encrypted store secrets.
|
|
5
3
|
|
|
6
4
|
// ---- Helpers ----
|
|
@@ -264,270 +262,6 @@ export async function handleEnrichment(sb, args, storeId) {
|
|
|
264
262
|
}
|
|
265
263
|
}
|
|
266
264
|
|
|
267
|
-
// ---- CHECK_XONPLUS: xonPlus real-time breach monitoring ----
|
|
268
|
-
case "check_xonplus":
|
|
269
|
-
{
|
|
270
|
-
const customerId = args.customer_id;
|
|
271
|
-
const email = args.email;
|
|
272
|
-
if (!customerId) return {
|
|
273
|
-
success: false,
|
|
274
|
-
error: "customer_id is required"
|
|
275
|
-
};
|
|
276
|
-
if (!email) return {
|
|
277
|
-
success: false,
|
|
278
|
-
error: "email is required"
|
|
279
|
-
};
|
|
280
|
-
const apiKey = await getSecret(sb, "xonplus_api_key", sid);
|
|
281
|
-
if (!apiKey) return {
|
|
282
|
-
success: false,
|
|
283
|
-
error: "xonPlus API key not configured. Add 'xonplus_api_key' to store secrets."
|
|
284
|
-
};
|
|
285
|
-
try {
|
|
286
|
-
const resp = await fetch(`https://api.xposedornot.com/v1/check-email/${encodeURIComponent(email)}`, {
|
|
287
|
-
method: "GET",
|
|
288
|
-
headers: {
|
|
289
|
-
"x-api-key": apiKey,
|
|
290
|
-
Accept: "application/json"
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
if (!resp.ok) {
|
|
294
|
-
const text = await resp.text().catch(() => "");
|
|
295
|
-
if (resp.status === 404) {
|
|
296
|
-
return {
|
|
297
|
-
success: true,
|
|
298
|
-
data: {
|
|
299
|
-
email,
|
|
300
|
-
total_breaches: 0,
|
|
301
|
-
breaches: []
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
return {
|
|
306
|
-
success: false,
|
|
307
|
-
error: `xonPlus API error ${resp.status}: ${text.substring(0, 500)}`
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
const xonData = await resp.json();
|
|
311
|
-
const breaches = xonData.breaches || xonData.ExposedBreaches?.breaches_details || [];
|
|
312
|
-
|
|
313
|
-
// Store each breach record
|
|
314
|
-
const inserted = [];
|
|
315
|
-
for (const breach of breaches) {
|
|
316
|
-
const record = {
|
|
317
|
-
customer_id: customerId,
|
|
318
|
-
store_id: sid,
|
|
319
|
-
breach_name: breach.breach || breach.name || "unknown",
|
|
320
|
-
breach_domain: breach.domain || null,
|
|
321
|
-
breach_date: breach.xposed_date || breach.date || null,
|
|
322
|
-
data_classes: breach.xposed_data ? breach.xposed_data.split(",").map(s => s.trim()) : null,
|
|
323
|
-
breach_source: "xonplus",
|
|
324
|
-
raw_data: breach,
|
|
325
|
-
discovered_at: nowISO(),
|
|
326
|
-
created_at: nowISO()
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
// Deduplicate by customer_id + breach_name + breach_source
|
|
330
|
-
const {
|
|
331
|
-
data: existing
|
|
332
|
-
} = await sb.from("customer_breach_records").select("id").eq("customer_id", customerId).eq("breach_name", record.breach_name).eq("breach_source", "xonplus").maybeSingle();
|
|
333
|
-
if (!existing) {
|
|
334
|
-
const {
|
|
335
|
-
data,
|
|
336
|
-
error
|
|
337
|
-
} = await sb.from("customer_breach_records").insert(record).select().single();
|
|
338
|
-
if (!error && data) inserted.push(data);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return {
|
|
342
|
-
success: true,
|
|
343
|
-
data: {
|
|
344
|
-
email,
|
|
345
|
-
total_breaches: breaches.length,
|
|
346
|
-
new_breaches: inserted.length,
|
|
347
|
-
risk_metrics: xonData.BreachMetrics || null,
|
|
348
|
-
breaches: inserted.length > 0 ? inserted : breaches
|
|
349
|
-
}
|
|
350
|
-
};
|
|
351
|
-
} catch (err) {
|
|
352
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
353
|
-
return {
|
|
354
|
-
success: false,
|
|
355
|
-
error: `xonPlus breach check failed: ${msg}`
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// ---- CHECK_BREACHES: HIBP Breach Check ----
|
|
361
|
-
case "check_breaches":
|
|
362
|
-
{
|
|
363
|
-
const customerId = args.customer_id;
|
|
364
|
-
const email = args.email;
|
|
365
|
-
if (!customerId) return {
|
|
366
|
-
success: false,
|
|
367
|
-
error: "customer_id is required"
|
|
368
|
-
};
|
|
369
|
-
if (!email) return {
|
|
370
|
-
success: false,
|
|
371
|
-
error: "email is required"
|
|
372
|
-
};
|
|
373
|
-
const apiKey = await getSecret(sb, "hibp_api_key", sid);
|
|
374
|
-
if (!apiKey) return {
|
|
375
|
-
success: false,
|
|
376
|
-
error: "HIBP API key not configured. Add 'hibp_api_key' to store secrets."
|
|
377
|
-
};
|
|
378
|
-
try {
|
|
379
|
-
const resp = await fetch(`https://haveibeenpwned.com/api/v3/breachedaccount/${encodeURIComponent(email)}?truncateResponse=false`, {
|
|
380
|
-
method: "GET",
|
|
381
|
-
headers: {
|
|
382
|
-
"hibp-api-key": apiKey,
|
|
383
|
-
"User-Agent": "WhaleTools-DataProtection",
|
|
384
|
-
Accept: "application/json"
|
|
385
|
-
}
|
|
386
|
-
});
|
|
387
|
-
let breaches = [];
|
|
388
|
-
if (resp.status === 404) {
|
|
389
|
-
// No breaches found — that's good
|
|
390
|
-
breaches = [];
|
|
391
|
-
} else if (!resp.ok) {
|
|
392
|
-
const text = await resp.text().catch(() => "");
|
|
393
|
-
return {
|
|
394
|
-
success: false,
|
|
395
|
-
error: `HIBP API error ${resp.status}: ${text.substring(0, 500)}`
|
|
396
|
-
};
|
|
397
|
-
} else {
|
|
398
|
-
breaches = await resp.json();
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Store each breach record
|
|
402
|
-
const inserted = [];
|
|
403
|
-
for (const breach of breaches) {
|
|
404
|
-
const record = {
|
|
405
|
-
customer_id: customerId,
|
|
406
|
-
store_id: sid,
|
|
407
|
-
breach_name: breach.Name,
|
|
408
|
-
breach_domain: breach.Domain,
|
|
409
|
-
breach_date: breach.BreachDate,
|
|
410
|
-
data_classes: breach.DataClasses,
|
|
411
|
-
description: breach.Description,
|
|
412
|
-
is_verified: breach.IsVerified,
|
|
413
|
-
breach_source: "hibp",
|
|
414
|
-
discovered_at: nowISO(),
|
|
415
|
-
created_at: nowISO()
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
// Upsert by customer_id + breach_name to avoid duplicates
|
|
419
|
-
const {
|
|
420
|
-
data: existing
|
|
421
|
-
} = await sb.from("customer_breach_records").select("id").eq("customer_id", customerId).eq("breach_name", breach.Name).maybeSingle();
|
|
422
|
-
if (!existing) {
|
|
423
|
-
const {
|
|
424
|
-
data,
|
|
425
|
-
error
|
|
426
|
-
} = await sb.from("customer_breach_records").insert(record).select().single();
|
|
427
|
-
if (!error && data) inserted.push(data);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
return {
|
|
431
|
-
success: true,
|
|
432
|
-
data: {
|
|
433
|
-
email,
|
|
434
|
-
total_breaches: breaches.length,
|
|
435
|
-
new_breaches: inserted.length,
|
|
436
|
-
breaches: inserted.length > 0 ? inserted : breaches
|
|
437
|
-
}
|
|
438
|
-
};
|
|
439
|
-
} catch (err) {
|
|
440
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
441
|
-
return {
|
|
442
|
-
success: false,
|
|
443
|
-
error: `HIBP breach check failed: ${msg}`
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// ---- CHECK_DEHASHED: DeHashed credential exposure search ----
|
|
449
|
-
case "check_dehashed":
|
|
450
|
-
{
|
|
451
|
-
const customerId = args.customer_id;
|
|
452
|
-
const email = args.email;
|
|
453
|
-
if (!customerId) return {
|
|
454
|
-
success: false,
|
|
455
|
-
error: "customer_id is required"
|
|
456
|
-
};
|
|
457
|
-
if (!email) return {
|
|
458
|
-
success: false,
|
|
459
|
-
error: "email is required"
|
|
460
|
-
};
|
|
461
|
-
const apiKey = await getSecret(sb, "dehashed_api_key", sid);
|
|
462
|
-
if (!apiKey) return {
|
|
463
|
-
success: false,
|
|
464
|
-
error: "DeHashed API key not configured. Add 'dehashed_api_key' to store secrets."
|
|
465
|
-
};
|
|
466
|
-
try {
|
|
467
|
-
const resp = await fetch("https://api.dehashed.com/v2/search", {
|
|
468
|
-
method: "POST",
|
|
469
|
-
headers: {
|
|
470
|
-
"Content-Type": "application/json",
|
|
471
|
-
"DeHashed-Api-Key": apiKey
|
|
472
|
-
},
|
|
473
|
-
body: JSON.stringify({
|
|
474
|
-
query: `email:"${email}"`,
|
|
475
|
-
page: 1,
|
|
476
|
-
size: 100,
|
|
477
|
-
wildcard: false,
|
|
478
|
-
regex: false,
|
|
479
|
-
de_dupe: true
|
|
480
|
-
})
|
|
481
|
-
});
|
|
482
|
-
if (!resp.ok) {
|
|
483
|
-
const text = await resp.text().catch(() => "");
|
|
484
|
-
return {
|
|
485
|
-
success: false,
|
|
486
|
-
error: `DeHashed API error ${resp.status}: ${text.substring(0, 500)}`
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
const result = await resp.json();
|
|
490
|
-
const entries = result.entries || [];
|
|
491
|
-
|
|
492
|
-
// Store each as a breach record with source=dehashed
|
|
493
|
-
const inserted = [];
|
|
494
|
-
for (const entry of entries) {
|
|
495
|
-
const record = {
|
|
496
|
-
customer_id: customerId,
|
|
497
|
-
store_id: sid,
|
|
498
|
-
breach_name: entry.database_name || "unknown",
|
|
499
|
-
breach_domain: entry.domain || null,
|
|
500
|
-
breach_date: entry.obtained_date || null,
|
|
501
|
-
data_classes: entry.type ? [entry.type] : null,
|
|
502
|
-
breach_source: "dehashed",
|
|
503
|
-
raw_data: entry,
|
|
504
|
-
discovered_at: nowISO(),
|
|
505
|
-
created_at: nowISO()
|
|
506
|
-
};
|
|
507
|
-
const {
|
|
508
|
-
data,
|
|
509
|
-
error
|
|
510
|
-
} = await sb.from("customer_breach_records").insert(record).select().single();
|
|
511
|
-
if (!error && data) inserted.push(data);
|
|
512
|
-
}
|
|
513
|
-
return {
|
|
514
|
-
success: true,
|
|
515
|
-
data: {
|
|
516
|
-
email,
|
|
517
|
-
total_results: entries.length,
|
|
518
|
-
stored: inserted.length,
|
|
519
|
-
entries: inserted
|
|
520
|
-
}
|
|
521
|
-
};
|
|
522
|
-
} catch (err) {
|
|
523
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
524
|
-
return {
|
|
525
|
-
success: false,
|
|
526
|
-
error: `DeHashed check failed: ${msg}`
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
265
|
// ---- GET_ENRICHMENT: Read enrichment profiles ----
|
|
532
266
|
case "get_enrichment":
|
|
533
267
|
{
|
|
@@ -557,364 +291,10 @@ export async function handleEnrichment(sb, args, storeId) {
|
|
|
557
291
|
}
|
|
558
292
|
};
|
|
559
293
|
}
|
|
560
|
-
|
|
561
|
-
// ---- GET_BREACHES: Read breach records ----
|
|
562
|
-
case "get_breaches":
|
|
563
|
-
{
|
|
564
|
-
const customerId = args.customer_id;
|
|
565
|
-
if (!customerId) return {
|
|
566
|
-
success: false,
|
|
567
|
-
error: "customer_id is required"
|
|
568
|
-
};
|
|
569
|
-
let q = sb.from("customer_breach_records").select("*").eq("customer_id", customerId).eq("store_id", sid).order("discovered_at", {
|
|
570
|
-
ascending: false
|
|
571
|
-
});
|
|
572
|
-
if (args.source) q = q.eq("breach_source", args.source);
|
|
573
|
-
const limit = args.limit || 50;
|
|
574
|
-
q = q.limit(limit);
|
|
575
|
-
const {
|
|
576
|
-
data,
|
|
577
|
-
error
|
|
578
|
-
} = await q;
|
|
579
|
-
return error ? {
|
|
580
|
-
success: false,
|
|
581
|
-
error: error.message
|
|
582
|
-
} : {
|
|
583
|
-
success: true,
|
|
584
|
-
data: {
|
|
585
|
-
count: data?.length,
|
|
586
|
-
records: data
|
|
587
|
-
}
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// ---- GET_EXPOSURES: Read broker exposures ----
|
|
592
|
-
case "get_exposures":
|
|
593
|
-
{
|
|
594
|
-
const customerId = args.customer_id;
|
|
595
|
-
if (!customerId) return {
|
|
596
|
-
success: false,
|
|
597
|
-
error: "customer_id is required"
|
|
598
|
-
};
|
|
599
|
-
let q = sb.from("customer_exposures").select("*").eq("customer_id", customerId).eq("store_id", sid).order("first_seen_at", {
|
|
600
|
-
ascending: false
|
|
601
|
-
});
|
|
602
|
-
if (args.status) q = q.eq("status", args.status);
|
|
603
|
-
if (args.source_name || args.broker) q = q.eq("source_name", args.source_name || args.broker);
|
|
604
|
-
const limit = args.limit || 50;
|
|
605
|
-
q = q.limit(limit);
|
|
606
|
-
const {
|
|
607
|
-
data,
|
|
608
|
-
error
|
|
609
|
-
} = await q;
|
|
610
|
-
return error ? {
|
|
611
|
-
success: false,
|
|
612
|
-
error: error.message
|
|
613
|
-
} : {
|
|
614
|
-
success: true,
|
|
615
|
-
data: {
|
|
616
|
-
count: data?.length,
|
|
617
|
-
records: data
|
|
618
|
-
}
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// ---- GET_REMOVAL_STATUS: Read removal requests ----
|
|
623
|
-
case "get_removal_status":
|
|
624
|
-
{
|
|
625
|
-
const customerId = args.customer_id;
|
|
626
|
-
if (!customerId) return {
|
|
627
|
-
success: false,
|
|
628
|
-
error: "customer_id is required"
|
|
629
|
-
};
|
|
630
|
-
let q = sb.from("customer_removal_requests").select("*").eq("customer_id", customerId).eq("store_id", sid).order("created_at", {
|
|
631
|
-
ascending: false
|
|
632
|
-
});
|
|
633
|
-
if (args.status) q = q.eq("status", args.status);
|
|
634
|
-
const limit = args.limit || 50;
|
|
635
|
-
q = q.limit(limit);
|
|
636
|
-
const {
|
|
637
|
-
data,
|
|
638
|
-
error
|
|
639
|
-
} = await q;
|
|
640
|
-
return error ? {
|
|
641
|
-
success: false,
|
|
642
|
-
error: error.message
|
|
643
|
-
} : {
|
|
644
|
-
success: true,
|
|
645
|
-
data: {
|
|
646
|
-
count: data?.length,
|
|
647
|
-
records: data
|
|
648
|
-
}
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// ---- GET_RISK_SCORE: Read latest risk score ----
|
|
653
|
-
case "get_risk_score":
|
|
654
|
-
{
|
|
655
|
-
const customerId = args.customer_id;
|
|
656
|
-
if (!customerId) return {
|
|
657
|
-
success: false,
|
|
658
|
-
error: "customer_id is required"
|
|
659
|
-
};
|
|
660
|
-
const {
|
|
661
|
-
data,
|
|
662
|
-
error
|
|
663
|
-
} = await sb.from("customer_risk_scores").select("*").eq("customer_id", customerId).eq("store_id", sid).order("calculated_at", {
|
|
664
|
-
ascending: false
|
|
665
|
-
}).limit(1).maybeSingle();
|
|
666
|
-
return error ? {
|
|
667
|
-
success: false,
|
|
668
|
-
error: error.message
|
|
669
|
-
} : {
|
|
670
|
-
success: true,
|
|
671
|
-
data
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// ---- STORE_SCAN_RESULTS: Insert scan results ----
|
|
676
|
-
case "store_scan_results":
|
|
677
|
-
{
|
|
678
|
-
const customerId = args.customer_id;
|
|
679
|
-
if (!customerId) return {
|
|
680
|
-
success: false,
|
|
681
|
-
error: "customer_id is required"
|
|
682
|
-
};
|
|
683
|
-
const record = {
|
|
684
|
-
customer_id: customerId,
|
|
685
|
-
store_id: sid,
|
|
686
|
-
scan_type: args.scan_type || "discovery",
|
|
687
|
-
scan_data: args.scan_data || {},
|
|
688
|
-
broker_count: args.broker_count ?? 0,
|
|
689
|
-
exposure_count: args.exposure_count ?? 0,
|
|
690
|
-
status: args.status || "completed",
|
|
691
|
-
created_at: nowISO()
|
|
692
|
-
};
|
|
693
|
-
if (args.scan_id) record.scan_id = args.scan_id;
|
|
694
|
-
if (args.file_path) record.file_path = args.file_path;
|
|
695
|
-
const {
|
|
696
|
-
data,
|
|
697
|
-
error
|
|
698
|
-
} = await sb.from("customer_scan_results").insert(record).select().single();
|
|
699
|
-
return error ? {
|
|
700
|
-
success: false,
|
|
701
|
-
error: `Failed to store scan results: ${error.message}`
|
|
702
|
-
} : {
|
|
703
|
-
success: true,
|
|
704
|
-
data
|
|
705
|
-
};
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// ---- STORE_EXPOSURE: Insert a broker exposure ----
|
|
709
|
-
case "store_exposure":
|
|
710
|
-
{
|
|
711
|
-
const customerId = args.customer_id;
|
|
712
|
-
const sourceName = args.source_name || args.broker;
|
|
713
|
-
if (!customerId) return {
|
|
714
|
-
success: false,
|
|
715
|
-
error: "customer_id is required"
|
|
716
|
-
};
|
|
717
|
-
if (!sourceName) return {
|
|
718
|
-
success: false,
|
|
719
|
-
error: "source_name is required"
|
|
720
|
-
};
|
|
721
|
-
|
|
722
|
-
// Check for existing exposure to avoid duplicates
|
|
723
|
-
const {
|
|
724
|
-
data: existing
|
|
725
|
-
} = await sb.from("customer_exposures").select("id").eq("customer_id", customerId).eq("store_id", sid).eq("source_name", sourceName).maybeSingle();
|
|
726
|
-
if (existing) {
|
|
727
|
-
// Update existing record
|
|
728
|
-
const updates = {};
|
|
729
|
-
if (args.source_url) updates.source_url = args.source_url;
|
|
730
|
-
if (args.data_types_exposed) updates.data_types_exposed = args.data_types_exposed;
|
|
731
|
-
if (args.status) updates.status = args.status;
|
|
732
|
-
const {
|
|
733
|
-
data,
|
|
734
|
-
error
|
|
735
|
-
} = await sb.from("customer_exposures").update(updates).eq("id", existing.id).select().single();
|
|
736
|
-
return error ? {
|
|
737
|
-
success: false,
|
|
738
|
-
error: `Failed to update exposure: ${error.message}`
|
|
739
|
-
} : {
|
|
740
|
-
success: true,
|
|
741
|
-
data: {
|
|
742
|
-
...data,
|
|
743
|
-
_note: "Updated existing exposure record"
|
|
744
|
-
}
|
|
745
|
-
};
|
|
746
|
-
}
|
|
747
|
-
const record = {
|
|
748
|
-
customer_id: customerId,
|
|
749
|
-
store_id: sid,
|
|
750
|
-
source_name: sourceName,
|
|
751
|
-
source_type: args.source_type || "data_broker",
|
|
752
|
-
status: args.status || "found",
|
|
753
|
-
first_seen_at: nowISO(),
|
|
754
|
-
created_at: nowISO()
|
|
755
|
-
};
|
|
756
|
-
if (args.source_url) record.source_url = args.source_url;
|
|
757
|
-
if (args.data_types_exposed) record.data_types_exposed = args.data_types_exposed;
|
|
758
|
-
if (args.scan_id) record.scan_id = args.scan_id;
|
|
759
|
-
const {
|
|
760
|
-
data,
|
|
761
|
-
error
|
|
762
|
-
} = await sb.from("customer_exposures").insert(record).select().single();
|
|
763
|
-
return error ? {
|
|
764
|
-
success: false,
|
|
765
|
-
error: `Failed to store exposure: ${error.message}`
|
|
766
|
-
} : {
|
|
767
|
-
success: true,
|
|
768
|
-
data
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
// ---- UPDATE_EXPOSURE_STATUS: Update exposure status ----
|
|
773
|
-
case "update_exposure_status":
|
|
774
|
-
{
|
|
775
|
-
const exposureId = args.exposure_id;
|
|
776
|
-
if (!exposureId) return {
|
|
777
|
-
success: false,
|
|
778
|
-
error: "exposure_id is required"
|
|
779
|
-
};
|
|
780
|
-
const updates = {};
|
|
781
|
-
if (args.status) updates.status = args.status;
|
|
782
|
-
if (args.status === "removed") {
|
|
783
|
-
updates.removed_at = nowISO();
|
|
784
|
-
}
|
|
785
|
-
const {
|
|
786
|
-
data,
|
|
787
|
-
error
|
|
788
|
-
} = await sb.from("customer_exposures").update(updates).eq("id", exposureId).eq("store_id", sid).select().single();
|
|
789
|
-
return error ? {
|
|
790
|
-
success: false,
|
|
791
|
-
error: `Failed to update exposure: ${error.message}`
|
|
792
|
-
} : {
|
|
793
|
-
success: true,
|
|
794
|
-
data
|
|
795
|
-
};
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// ---- STORE_REMOVAL_REQUEST: Insert a removal request ----
|
|
799
|
-
case "store_removal_request":
|
|
800
|
-
{
|
|
801
|
-
const customerId = args.customer_id;
|
|
802
|
-
const brokerName = args.broker_name || args.source_name || args.broker;
|
|
803
|
-
if (!customerId) return {
|
|
804
|
-
success: false,
|
|
805
|
-
error: "customer_id is required"
|
|
806
|
-
};
|
|
807
|
-
if (!brokerName) return {
|
|
808
|
-
success: false,
|
|
809
|
-
error: "source_name is required"
|
|
810
|
-
};
|
|
811
|
-
const record = {
|
|
812
|
-
customer_id: customerId,
|
|
813
|
-
store_id: sid,
|
|
814
|
-
broker_name: brokerName,
|
|
815
|
-
removal_method: args.removal_method || args.method || "manual",
|
|
816
|
-
status: args.status || "pending",
|
|
817
|
-
created_at: nowISO(),
|
|
818
|
-
updated_at: nowISO()
|
|
819
|
-
};
|
|
820
|
-
if (args.exposure_id) record.exposure_id = args.exposure_id;
|
|
821
|
-
if (args.request_data) record.request_data = args.request_data;
|
|
822
|
-
if (args.submitted_at) record.submitted_at = args.submitted_at;
|
|
823
|
-
if (args.confirmation_id) record.confirmation_id = args.confirmation_id;
|
|
824
|
-
const {
|
|
825
|
-
data,
|
|
826
|
-
error
|
|
827
|
-
} = await sb.from("customer_removal_requests").insert(record).select().single();
|
|
828
|
-
return error ? {
|
|
829
|
-
success: false,
|
|
830
|
-
error: `Failed to store removal request: ${error.message}`
|
|
831
|
-
} : {
|
|
832
|
-
success: true,
|
|
833
|
-
data
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// ---- CALCULATE_RISK_SCORE: Compute and store a risk score ----
|
|
838
|
-
case "calculate_risk_score":
|
|
839
|
-
{
|
|
840
|
-
const customerId = args.customer_id;
|
|
841
|
-
if (!customerId) return {
|
|
842
|
-
success: false,
|
|
843
|
-
error: "customer_id is required"
|
|
844
|
-
};
|
|
845
|
-
|
|
846
|
-
// Gather data for risk calculation
|
|
847
|
-
const [exposuresResult, breachesResult, removalsResult] = await Promise.all([sb.from("customer_exposures").select("id, source_name, status").eq("customer_id", customerId).eq("store_id", sid), sb.from("customer_breach_records").select("id, breach_name, data_classes, is_verified").eq("customer_id", customerId).eq("store_id", sid), sb.from("customer_removal_requests").select("id, status, broker_name").eq("customer_id", customerId).eq("store_id", sid)]);
|
|
848
|
-
const exposures = exposuresResult.data || [];
|
|
849
|
-
const breaches = breachesResult.data || [];
|
|
850
|
-
const removals = removalsResult.data || [];
|
|
851
|
-
|
|
852
|
-
// Score calculation:
|
|
853
|
-
// - Each active exposure: +10 points
|
|
854
|
-
// - Each removed exposure: -5 points (still contributes slightly)
|
|
855
|
-
// - Each breach: +15 points
|
|
856
|
-
// - Each sensitive breach: +25 points (instead of 15)
|
|
857
|
-
// - Each verified breach: +5 bonus
|
|
858
|
-
// - Each completed removal: -8 points
|
|
859
|
-
// - Base floor: 0, cap: 100
|
|
860
|
-
|
|
861
|
-
let score = 0;
|
|
862
|
-
|
|
863
|
-
// Exposure scoring
|
|
864
|
-
const activeExposures = exposures.filter(e => e.status !== "removed");
|
|
865
|
-
const removedExposures = exposures.filter(e => e.status === "removed");
|
|
866
|
-
score += activeExposures.length * 10;
|
|
867
|
-
score += removedExposures.length * 2;
|
|
868
|
-
|
|
869
|
-
// Breach scoring
|
|
870
|
-
for (const breach of breaches) {
|
|
871
|
-
score += 15;
|
|
872
|
-
if (breach.is_verified) {
|
|
873
|
-
score += 5;
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
// Removal credit
|
|
878
|
-
const completedRemovals = removals.filter(r => r.status === "completed" || r.status === "confirmed");
|
|
879
|
-
score -= completedRemovals.length * 8;
|
|
880
|
-
|
|
881
|
-
// Clamp to 0-100
|
|
882
|
-
score = Math.max(0, Math.min(100, score));
|
|
883
|
-
|
|
884
|
-
// Determine risk level
|
|
885
|
-
let riskLevel;
|
|
886
|
-
if (score >= 75) riskLevel = "critical";else if (score >= 50) riskLevel = "high";else if (score >= 25) riskLevel = "medium";else riskLevel = "low";
|
|
887
|
-
const riskRecord = {
|
|
888
|
-
customer_id: customerId,
|
|
889
|
-
store_id: sid,
|
|
890
|
-
overall_score: score,
|
|
891
|
-
risk_level: riskLevel,
|
|
892
|
-
factors: {
|
|
893
|
-
active_exposures: activeExposures.length,
|
|
894
|
-
removed_exposures: removedExposures.length,
|
|
895
|
-
total_breaches: breaches.length,
|
|
896
|
-
completed_removals: completedRemovals.length,
|
|
897
|
-
pending_removals: removals.filter(r => r.status === "pending").length
|
|
898
|
-
},
|
|
899
|
-
calculated_at: nowISO(),
|
|
900
|
-
created_at: nowISO()
|
|
901
|
-
};
|
|
902
|
-
const {
|
|
903
|
-
data,
|
|
904
|
-
error
|
|
905
|
-
} = await sb.from("customer_risk_scores").insert(riskRecord).select().single();
|
|
906
|
-
return error ? {
|
|
907
|
-
success: false,
|
|
908
|
-
error: `Failed to store risk score: ${error.message}`
|
|
909
|
-
} : {
|
|
910
|
-
success: true,
|
|
911
|
-
data
|
|
912
|
-
};
|
|
913
|
-
}
|
|
914
294
|
default:
|
|
915
295
|
return {
|
|
916
296
|
success: false,
|
|
917
|
-
error: `Unknown enrichment action: ${action}. Valid: enrich_person, enrich_linkedin,
|
|
297
|
+
error: `Unknown enrichment action: ${action}. Valid: enrich_person, enrich_linkedin, get_enrichment`
|
|
918
298
|
};
|
|
919
299
|
}
|
|
920
300
|
}
|