whale-code 6.5.8 → 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.
Files changed (99) hide show
  1. package/dist/cli/services/agent-loop.js +26 -2
  2. package/dist/cli/services/agent-loop.js.map +1 -1
  3. package/dist/cli/services/hooks.js +2 -1
  4. package/dist/cli/services/hooks.js.map +1 -1
  5. package/dist/cli/services/telemetry-spans.js +1 -0
  6. package/dist/cli/services/telemetry-spans.js.map +1 -1
  7. package/dist/cli/services/telemetry.d.ts +23 -0
  8. package/dist/cli/services/telemetry.js +45 -1
  9. package/dist/cli/services/telemetry.js.map +1 -1
  10. package/dist/server/handlers/__test-utils__/test-db.d.ts +17 -3
  11. package/dist/server/handlers/__test-utils__/test-db.js +113 -14
  12. package/dist/server/handlers/__test-utils__/test-db.js.map +1 -1
  13. package/dist/server/handlers/affiliates.d.ts +9 -0
  14. package/dist/server/handlers/affiliates.js +197 -0
  15. package/dist/server/handlers/affiliates.js.map +1 -0
  16. package/dist/server/handlers/api-docs.d.ts +4 -2
  17. package/dist/server/handlers/api-docs.js +204 -1681
  18. package/dist/server/handlers/api-docs.js.map +1 -1
  19. package/dist/server/handlers/campaigns.d.ts +9 -0
  20. package/dist/server/handlers/campaigns.js +237 -0
  21. package/dist/server/handlers/campaigns.js.map +1 -0
  22. package/dist/server/handlers/catalog-schemas.js +9 -9
  23. package/dist/server/handlers/catalog-schemas.js.map +1 -1
  24. package/dist/server/handlers/catalog.js +1 -1
  25. package/dist/server/handlers/catalog.js.map +1 -1
  26. package/dist/server/handlers/comms-documents.js +28 -2
  27. package/dist/server/handlers/comms-documents.js.map +1 -1
  28. package/dist/server/handlers/comms-pdf-generation.js +25 -3
  29. package/dist/server/handlers/comms-pdf-generation.js.map +1 -1
  30. package/dist/server/handlers/comms-pdf-helpers.js +4 -4
  31. package/dist/server/handlers/comms-pdf-helpers.js.map +1 -1
  32. package/dist/server/handlers/comms.d.ts +100 -0
  33. package/dist/server/handlers/comms.js +146 -12
  34. package/dist/server/handlers/comms.js.map +1 -1
  35. package/dist/server/handlers/coupons.d.ts +9 -0
  36. package/dist/server/handlers/coupons.js +220 -0
  37. package/dist/server/handlers/coupons.js.map +1 -0
  38. package/dist/server/handlers/embeddings.js +1 -1
  39. package/dist/server/handlers/embeddings.js.map +1 -1
  40. package/dist/server/handlers/enrichment.js +2 -622
  41. package/dist/server/handlers/enrichment.js.map +1 -1
  42. package/dist/server/handlers/fulfillment.d.ts +9 -0
  43. package/dist/server/handlers/fulfillment.js +209 -0
  44. package/dist/server/handlers/fulfillment.js.map +1 -0
  45. package/dist/server/handlers/google-ads.d.ts +24 -0
  46. package/dist/server/handlers/google-ads.js +2199 -0
  47. package/dist/server/handlers/google-ads.js.map +1 -0
  48. package/dist/server/handlers/invoices.d.ts +9 -0
  49. package/dist/server/handlers/invoices.js +252 -0
  50. package/dist/server/handlers/invoices.js.map +1 -0
  51. package/dist/server/handlers/loyalty.d.ts +9 -0
  52. package/dist/server/handlers/loyalty.js +197 -0
  53. package/dist/server/handlers/loyalty.js.map +1 -0
  54. package/dist/server/handlers/meta-ads-graph-api.js +18 -3
  55. package/dist/server/handlers/meta-ads-graph-api.js.map +1 -1
  56. package/dist/server/handlers/phone.d.ts +9 -0
  57. package/dist/server/handlers/phone.js +197 -0
  58. package/dist/server/handlers/phone.js.map +1 -0
  59. package/dist/server/handlers/pipeline.d.ts +9 -0
  60. package/dist/server/handlers/pipeline.js +277 -0
  61. package/dist/server/handlers/pipeline.js.map +1 -0
  62. package/dist/server/handlers/qr-codes.d.ts +9 -0
  63. package/dist/server/handlers/qr-codes.js +198 -0
  64. package/dist/server/handlers/qr-codes.js.map +1 -0
  65. package/dist/server/handlers/reviews.d.ts +9 -0
  66. package/dist/server/handlers/reviews.js +171 -0
  67. package/dist/server/handlers/reviews.js.map +1 -0
  68. package/dist/server/handlers/segments.d.ts +9 -0
  69. package/dist/server/handlers/segments.js +229 -0
  70. package/dist/server/handlers/segments.js.map +1 -0
  71. package/dist/server/handlers/social.d.ts +9 -0
  72. package/dist/server/handlers/social.js +81 -0
  73. package/dist/server/handlers/social.js.map +1 -0
  74. package/dist/server/handlers/tax.d.ts +9 -0
  75. package/dist/server/handlers/tax.js +182 -0
  76. package/dist/server/handlers/tax.js.map +1 -0
  77. package/dist/server/handlers/wallet.d.ts +9 -0
  78. package/dist/server/handlers/wallet.js +203 -0
  79. package/dist/server/handlers/wallet.js.map +1 -0
  80. package/dist/server/handlers/webhooks-mgmt.d.ts +9 -0
  81. package/dist/server/handlers/webhooks-mgmt.js +181 -0
  82. package/dist/server/handlers/webhooks-mgmt.js.map +1 -0
  83. package/dist/server/handlers/wholesale.d.ts +9 -0
  84. package/dist/server/handlers/wholesale.js +219 -0
  85. package/dist/server/handlers/wholesale.js.map +1 -0
  86. package/dist/server/index.js +20 -9
  87. package/dist/server/index.js.map +1 -1
  88. package/dist/server/lib/clickhouse-buffer.js +1 -0
  89. package/dist/server/lib/clickhouse-buffer.js.map +1 -1
  90. package/dist/server/lib/coa-renderer.d.ts +1 -1
  91. package/dist/server/lib/coa-renderer.js +32 -10
  92. package/dist/server/lib/coa-renderer.js.map +1 -1
  93. package/dist/server/server-worker.d.ts +1 -0
  94. package/dist/server/server-worker.js +464 -3
  95. package/dist/server/server-worker.js.map +1 -1
  96. package/dist/server/tool-router.js +118 -4
  97. package/dist/server/tool-router.js.map +1 -1
  98. package/package.json +26 -3
  99. package/vendor/ink/package.json +0 -2
@@ -1,6 +1,4 @@
1
- // server/handlers/enrichment.ts — Customer enrichment, breach checking, and data protection CRUD
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, check_breaches, check_dehashed, check_xonplus, get_enrichment, get_breaches, get_exposures, get_removal_status, get_risk_score, store_scan_results, store_exposure, update_exposure_status, store_removal_request, calculate_risk_score`
297
+ error: `Unknown enrichment action: ${action}. Valid: enrich_person, enrich_linkedin, get_enrichment`
918
298
  };
919
299
  }
920
300
  }