whale-code 6.4.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 (319) hide show
  1. package/README.md +95 -0
  2. package/bin/swag-agent.js +9 -0
  3. package/bin/swagmanager-mcp.js +321 -0
  4. package/dist/cli/app.d.ts +26 -0
  5. package/dist/cli/app.js +64 -0
  6. package/dist/cli/chat/AgentSelector.d.ts +14 -0
  7. package/dist/cli/chat/AgentSelector.js +14 -0
  8. package/dist/cli/chat/ChatApp.d.ts +9 -0
  9. package/dist/cli/chat/ChatApp.js +267 -0
  10. package/dist/cli/chat/ChatInput.d.ts +39 -0
  11. package/dist/cli/chat/ChatInput.js +509 -0
  12. package/dist/cli/chat/MarkdownText.d.ts +10 -0
  13. package/dist/cli/chat/MarkdownText.js +20 -0
  14. package/dist/cli/chat/MessageList.d.ts +37 -0
  15. package/dist/cli/chat/MessageList.js +80 -0
  16. package/dist/cli/chat/ModelSelector.d.ts +20 -0
  17. package/dist/cli/chat/ModelSelector.js +73 -0
  18. package/dist/cli/chat/RewindViewer.d.ts +26 -0
  19. package/dist/cli/chat/RewindViewer.js +185 -0
  20. package/dist/cli/chat/StoreSelector.d.ts +14 -0
  21. package/dist/cli/chat/StoreSelector.js +24 -0
  22. package/dist/cli/chat/StreamingText.d.ts +12 -0
  23. package/dist/cli/chat/StreamingText.js +12 -0
  24. package/dist/cli/chat/SubagentPanel.d.ts +45 -0
  25. package/dist/cli/chat/SubagentPanel.js +110 -0
  26. package/dist/cli/chat/TeamPanel.d.ts +21 -0
  27. package/dist/cli/chat/TeamPanel.js +42 -0
  28. package/dist/cli/chat/ToolIndicator.d.ts +25 -0
  29. package/dist/cli/chat/ToolIndicator.js +436 -0
  30. package/dist/cli/chat/hooks/useAgentLoop.d.ts +39 -0
  31. package/dist/cli/chat/hooks/useAgentLoop.js +382 -0
  32. package/dist/cli/chat/hooks/useSlashCommands.d.ts +37 -0
  33. package/dist/cli/chat/hooks/useSlashCommands.js +387 -0
  34. package/dist/cli/commands/config-cmd.d.ts +10 -0
  35. package/dist/cli/commands/config-cmd.js +99 -0
  36. package/dist/cli/commands/doctor.d.ts +14 -0
  37. package/dist/cli/commands/doctor.js +172 -0
  38. package/dist/cli/commands/init.d.ts +16 -0
  39. package/dist/cli/commands/init.js +278 -0
  40. package/dist/cli/commands/mcp.d.ts +12 -0
  41. package/dist/cli/commands/mcp.js +162 -0
  42. package/dist/cli/login/LoginApp.d.ts +7 -0
  43. package/dist/cli/login/LoginApp.js +157 -0
  44. package/dist/cli/print-mode.d.ts +31 -0
  45. package/dist/cli/print-mode.js +202 -0
  46. package/dist/cli/serve-mode.d.ts +37 -0
  47. package/dist/cli/serve-mode.js +636 -0
  48. package/dist/cli/services/agent-definitions.d.ts +25 -0
  49. package/dist/cli/services/agent-definitions.js +91 -0
  50. package/dist/cli/services/agent-events.d.ts +178 -0
  51. package/dist/cli/services/agent-events.js +175 -0
  52. package/dist/cli/services/agent-loop.d.ts +90 -0
  53. package/dist/cli/services/agent-loop.js +762 -0
  54. package/dist/cli/services/agent-worker-base.d.ts +97 -0
  55. package/dist/cli/services/agent-worker-base.js +220 -0
  56. package/dist/cli/services/auth-service.d.ts +30 -0
  57. package/dist/cli/services/auth-service.js +160 -0
  58. package/dist/cli/services/background-processes.d.ts +126 -0
  59. package/dist/cli/services/background-processes.js +318 -0
  60. package/dist/cli/services/browser-auth.d.ts +24 -0
  61. package/dist/cli/services/browser-auth.js +180 -0
  62. package/dist/cli/services/claude-md-loader.d.ts +16 -0
  63. package/dist/cli/services/claude-md-loader.js +58 -0
  64. package/dist/cli/services/config-store.d.ts +47 -0
  65. package/dist/cli/services/config-store.js +79 -0
  66. package/dist/cli/services/debug-log.d.ts +10 -0
  67. package/dist/cli/services/debug-log.js +52 -0
  68. package/dist/cli/services/error-logger.d.ts +58 -0
  69. package/dist/cli/services/error-logger.js +269 -0
  70. package/dist/cli/services/file-history.d.ts +21 -0
  71. package/dist/cli/services/file-history.js +83 -0
  72. package/dist/cli/services/format-server-response.d.ts +16 -0
  73. package/dist/cli/services/format-server-response.js +440 -0
  74. package/dist/cli/services/git-context.d.ts +11 -0
  75. package/dist/cli/services/git-context.js +66 -0
  76. package/dist/cli/services/hooks.d.ts +85 -0
  77. package/dist/cli/services/hooks.js +258 -0
  78. package/dist/cli/services/interactive-tools.d.ts +125 -0
  79. package/dist/cli/services/interactive-tools.js +260 -0
  80. package/dist/cli/services/keybinding-manager.d.ts +52 -0
  81. package/dist/cli/services/keybinding-manager.js +115 -0
  82. package/dist/cli/services/local-tools.d.ts +22 -0
  83. package/dist/cli/services/local-tools.js +697 -0
  84. package/dist/cli/services/lsp-manager.d.ts +18 -0
  85. package/dist/cli/services/lsp-manager.js +717 -0
  86. package/dist/cli/services/mcp-client.d.ts +48 -0
  87. package/dist/cli/services/mcp-client.js +157 -0
  88. package/dist/cli/services/memory-manager.d.ts +16 -0
  89. package/dist/cli/services/memory-manager.js +57 -0
  90. package/dist/cli/services/model-manager.d.ts +18 -0
  91. package/dist/cli/services/model-manager.js +71 -0
  92. package/dist/cli/services/model-router.d.ts +26 -0
  93. package/dist/cli/services/model-router.js +149 -0
  94. package/dist/cli/services/permission-modes.d.ts +13 -0
  95. package/dist/cli/services/permission-modes.js +43 -0
  96. package/dist/cli/services/rewind.d.ts +84 -0
  97. package/dist/cli/services/rewind.js +194 -0
  98. package/dist/cli/services/ripgrep.d.ts +28 -0
  99. package/dist/cli/services/ripgrep.js +138 -0
  100. package/dist/cli/services/sandbox.d.ts +29 -0
  101. package/dist/cli/services/sandbox.js +97 -0
  102. package/dist/cli/services/server-tools.d.ts +61 -0
  103. package/dist/cli/services/server-tools.js +543 -0
  104. package/dist/cli/services/session-persistence.d.ts +23 -0
  105. package/dist/cli/services/session-persistence.js +99 -0
  106. package/dist/cli/services/subagent-worker.d.ts +19 -0
  107. package/dist/cli/services/subagent-worker.js +41 -0
  108. package/dist/cli/services/subagent.d.ts +47 -0
  109. package/dist/cli/services/subagent.js +647 -0
  110. package/dist/cli/services/system-prompt.d.ts +7 -0
  111. package/dist/cli/services/system-prompt.js +198 -0
  112. package/dist/cli/services/team-lead.d.ts +73 -0
  113. package/dist/cli/services/team-lead.js +512 -0
  114. package/dist/cli/services/team-state.d.ts +77 -0
  115. package/dist/cli/services/team-state.js +398 -0
  116. package/dist/cli/services/teammate.d.ts +31 -0
  117. package/dist/cli/services/teammate.js +689 -0
  118. package/dist/cli/services/telemetry.d.ts +61 -0
  119. package/dist/cli/services/telemetry.js +209 -0
  120. package/dist/cli/services/tools/agent-tools.d.ts +14 -0
  121. package/dist/cli/services/tools/agent-tools.js +347 -0
  122. package/dist/cli/services/tools/file-ops.d.ts +15 -0
  123. package/dist/cli/services/tools/file-ops.js +487 -0
  124. package/dist/cli/services/tools/search-tools.d.ts +8 -0
  125. package/dist/cli/services/tools/search-tools.js +186 -0
  126. package/dist/cli/services/tools/shell-exec.d.ts +10 -0
  127. package/dist/cli/services/tools/shell-exec.js +168 -0
  128. package/dist/cli/services/tools/task-manager.d.ts +28 -0
  129. package/dist/cli/services/tools/task-manager.js +209 -0
  130. package/dist/cli/services/tools/web-tools.d.ts +11 -0
  131. package/dist/cli/services/tools/web-tools.js +395 -0
  132. package/dist/cli/setup/SetupApp.d.ts +9 -0
  133. package/dist/cli/setup/SetupApp.js +191 -0
  134. package/dist/cli/shared/MatrixIntro.d.ts +4 -0
  135. package/dist/cli/shared/MatrixIntro.js +83 -0
  136. package/dist/cli/shared/Theme.d.ts +74 -0
  137. package/dist/cli/shared/Theme.js +127 -0
  138. package/dist/cli/shared/WhaleBanner.d.ts +10 -0
  139. package/dist/cli/shared/WhaleBanner.js +12 -0
  140. package/dist/cli/shared/markdown.d.ts +21 -0
  141. package/dist/cli/shared/markdown.js +756 -0
  142. package/dist/cli/status/StatusApp.d.ts +4 -0
  143. package/dist/cli/status/StatusApp.js +105 -0
  144. package/dist/cli/stores/StoreApp.d.ts +7 -0
  145. package/dist/cli/stores/StoreApp.js +81 -0
  146. package/dist/index.d.ts +15 -0
  147. package/dist/index.js +538 -0
  148. package/dist/local-agent/connection.d.ts +48 -0
  149. package/dist/local-agent/connection.js +332 -0
  150. package/dist/local-agent/discovery.d.ts +18 -0
  151. package/dist/local-agent/discovery.js +146 -0
  152. package/dist/local-agent/executor.d.ts +34 -0
  153. package/dist/local-agent/executor.js +241 -0
  154. package/dist/local-agent/index.d.ts +14 -0
  155. package/dist/local-agent/index.js +198 -0
  156. package/dist/node/adapters/base.d.ts +35 -0
  157. package/dist/node/adapters/base.js +10 -0
  158. package/dist/node/adapters/discord.d.ts +29 -0
  159. package/dist/node/adapters/discord.js +299 -0
  160. package/dist/node/adapters/email.d.ts +23 -0
  161. package/dist/node/adapters/email.js +218 -0
  162. package/dist/node/adapters/imessage.d.ts +17 -0
  163. package/dist/node/adapters/imessage.js +118 -0
  164. package/dist/node/adapters/slack.d.ts +26 -0
  165. package/dist/node/adapters/slack.js +259 -0
  166. package/dist/node/adapters/sms.d.ts +23 -0
  167. package/dist/node/adapters/sms.js +161 -0
  168. package/dist/node/adapters/telegram.d.ts +17 -0
  169. package/dist/node/adapters/telegram.js +101 -0
  170. package/dist/node/adapters/webchat.d.ts +27 -0
  171. package/dist/node/adapters/webchat.js +160 -0
  172. package/dist/node/adapters/whatsapp.d.ts +28 -0
  173. package/dist/node/adapters/whatsapp.js +230 -0
  174. package/dist/node/cli.d.ts +2 -0
  175. package/dist/node/cli.js +325 -0
  176. package/dist/node/config.d.ts +17 -0
  177. package/dist/node/config.js +31 -0
  178. package/dist/node/runtime.d.ts +50 -0
  179. package/dist/node/runtime.js +351 -0
  180. package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +11 -0
  181. package/dist/server/handlers/__test-utils__/mock-supabase.js +393 -0
  182. package/dist/server/handlers/analytics.d.ts +17 -0
  183. package/dist/server/handlers/analytics.js +266 -0
  184. package/dist/server/handlers/api-keys.d.ts +6 -0
  185. package/dist/server/handlers/api-keys.js +221 -0
  186. package/dist/server/handlers/billing.d.ts +33 -0
  187. package/dist/server/handlers/billing.js +272 -0
  188. package/dist/server/handlers/browser.d.ts +10 -0
  189. package/dist/server/handlers/browser.js +517 -0
  190. package/dist/server/handlers/catalog.d.ts +99 -0
  191. package/dist/server/handlers/catalog.js +976 -0
  192. package/dist/server/handlers/comms.d.ts +254 -0
  193. package/dist/server/handlers/comms.js +588 -0
  194. package/dist/server/handlers/creations.d.ts +6 -0
  195. package/dist/server/handlers/creations.js +479 -0
  196. package/dist/server/handlers/crm.d.ts +89 -0
  197. package/dist/server/handlers/crm.js +538 -0
  198. package/dist/server/handlers/discovery.d.ts +6 -0
  199. package/dist/server/handlers/discovery.js +288 -0
  200. package/dist/server/handlers/embeddings.d.ts +92 -0
  201. package/dist/server/handlers/embeddings.js +197 -0
  202. package/dist/server/handlers/enrichment.d.ts +8 -0
  203. package/dist/server/handlers/enrichment.js +768 -0
  204. package/dist/server/handlers/image-gen.d.ts +6 -0
  205. package/dist/server/handlers/image-gen.js +409 -0
  206. package/dist/server/handlers/inventory.d.ts +319 -0
  207. package/dist/server/handlers/inventory.js +447 -0
  208. package/dist/server/handlers/kali.d.ts +10 -0
  209. package/dist/server/handlers/kali.js +210 -0
  210. package/dist/server/handlers/llm-providers.d.ts +6 -0
  211. package/dist/server/handlers/llm-providers.js +673 -0
  212. package/dist/server/handlers/local-agent.d.ts +6 -0
  213. package/dist/server/handlers/local-agent.js +118 -0
  214. package/dist/server/handlers/meta-ads.d.ts +111 -0
  215. package/dist/server/handlers/meta-ads.js +2279 -0
  216. package/dist/server/handlers/nodes.d.ts +33 -0
  217. package/dist/server/handlers/nodes.js +699 -0
  218. package/dist/server/handlers/operations.d.ts +138 -0
  219. package/dist/server/handlers/operations.js +131 -0
  220. package/dist/server/handlers/platform.d.ts +23 -0
  221. package/dist/server/handlers/platform.js +227 -0
  222. package/dist/server/handlers/supply-chain.d.ts +19 -0
  223. package/dist/server/handlers/supply-chain.js +327 -0
  224. package/dist/server/handlers/transcription.d.ts +17 -0
  225. package/dist/server/handlers/transcription.js +121 -0
  226. package/dist/server/handlers/video-gen.d.ts +6 -0
  227. package/dist/server/handlers/video-gen.js +466 -0
  228. package/dist/server/handlers/voice.d.ts +8 -0
  229. package/dist/server/handlers/voice.js +1146 -0
  230. package/dist/server/handlers/workflow-steps.d.ts +86 -0
  231. package/dist/server/handlers/workflow-steps.js +2349 -0
  232. package/dist/server/handlers/workflows.d.ts +7 -0
  233. package/dist/server/handlers/workflows.js +989 -0
  234. package/dist/server/index.d.ts +1 -0
  235. package/dist/server/index.js +2427 -0
  236. package/dist/server/lib/batch-client.d.ts +80 -0
  237. package/dist/server/lib/batch-client.js +467 -0
  238. package/dist/server/lib/code-worker-pool.d.ts +31 -0
  239. package/dist/server/lib/code-worker-pool.js +224 -0
  240. package/dist/server/lib/code-worker.d.ts +1 -0
  241. package/dist/server/lib/code-worker.js +188 -0
  242. package/dist/server/lib/compaction-service.d.ts +32 -0
  243. package/dist/server/lib/compaction-service.js +162 -0
  244. package/dist/server/lib/logger.d.ts +19 -0
  245. package/dist/server/lib/logger.js +46 -0
  246. package/dist/server/lib/otel.d.ts +38 -0
  247. package/dist/server/lib/otel.js +126 -0
  248. package/dist/server/lib/pg-rate-limiter.d.ts +21 -0
  249. package/dist/server/lib/pg-rate-limiter.js +86 -0
  250. package/dist/server/lib/prompt-sanitizer.d.ts +37 -0
  251. package/dist/server/lib/prompt-sanitizer.js +177 -0
  252. package/dist/server/lib/provider-capabilities.d.ts +85 -0
  253. package/dist/server/lib/provider-capabilities.js +190 -0
  254. package/dist/server/lib/provider-failover.d.ts +74 -0
  255. package/dist/server/lib/provider-failover.js +210 -0
  256. package/dist/server/lib/rate-limiter.d.ts +39 -0
  257. package/dist/server/lib/rate-limiter.js +147 -0
  258. package/dist/server/lib/server-agent-loop.d.ts +107 -0
  259. package/dist/server/lib/server-agent-loop.js +667 -0
  260. package/dist/server/lib/server-subagent.d.ts +78 -0
  261. package/dist/server/lib/server-subagent.js +203 -0
  262. package/dist/server/lib/session-checkpoint.d.ts +51 -0
  263. package/dist/server/lib/session-checkpoint.js +145 -0
  264. package/dist/server/lib/ssrf-guard.d.ts +13 -0
  265. package/dist/server/lib/ssrf-guard.js +240 -0
  266. package/dist/server/lib/supabase-client.d.ts +7 -0
  267. package/dist/server/lib/supabase-client.js +78 -0
  268. package/dist/server/lib/template-resolver.d.ts +31 -0
  269. package/dist/server/lib/template-resolver.js +215 -0
  270. package/dist/server/lib/utils.d.ts +16 -0
  271. package/dist/server/lib/utils.js +147 -0
  272. package/dist/server/local-agent-gateway.d.ts +82 -0
  273. package/dist/server/local-agent-gateway.js +426 -0
  274. package/dist/server/providers/anthropic.d.ts +20 -0
  275. package/dist/server/providers/anthropic.js +199 -0
  276. package/dist/server/providers/bedrock.d.ts +20 -0
  277. package/dist/server/providers/bedrock.js +194 -0
  278. package/dist/server/providers/gemini.d.ts +24 -0
  279. package/dist/server/providers/gemini.js +486 -0
  280. package/dist/server/providers/openai.d.ts +24 -0
  281. package/dist/server/providers/openai.js +522 -0
  282. package/dist/server/providers/registry.d.ts +32 -0
  283. package/dist/server/providers/registry.js +58 -0
  284. package/dist/server/providers/shared.d.ts +32 -0
  285. package/dist/server/providers/shared.js +124 -0
  286. package/dist/server/providers/types.d.ts +92 -0
  287. package/dist/server/providers/types.js +12 -0
  288. package/dist/server/proxy-handlers.d.ts +6 -0
  289. package/dist/server/proxy-handlers.js +89 -0
  290. package/dist/server/tool-router.d.ts +149 -0
  291. package/dist/server/tool-router.js +803 -0
  292. package/dist/server/validation.d.ts +24 -0
  293. package/dist/server/validation.js +301 -0
  294. package/dist/server/worker.d.ts +19 -0
  295. package/dist/server/worker.js +201 -0
  296. package/dist/setup.d.ts +8 -0
  297. package/dist/setup.js +181 -0
  298. package/dist/shared/agent-core.d.ts +157 -0
  299. package/dist/shared/agent-core.js +534 -0
  300. package/dist/shared/anthropic-types.d.ts +105 -0
  301. package/dist/shared/anthropic-types.js +7 -0
  302. package/dist/shared/api-client.d.ts +90 -0
  303. package/dist/shared/api-client.js +379 -0
  304. package/dist/shared/constants.d.ts +33 -0
  305. package/dist/shared/constants.js +80 -0
  306. package/dist/shared/sse-parser.d.ts +26 -0
  307. package/dist/shared/sse-parser.js +259 -0
  308. package/dist/shared/tool-dispatch.d.ts +52 -0
  309. package/dist/shared/tool-dispatch.js +191 -0
  310. package/dist/shared/types.d.ts +72 -0
  311. package/dist/shared/types.js +7 -0
  312. package/dist/updater.d.ts +25 -0
  313. package/dist/updater.js +140 -0
  314. package/dist/webchat/widget.d.ts +0 -0
  315. package/dist/webchat/widget.js +397 -0
  316. package/package.json +95 -0
  317. package/src/cli/services/builtin-skills/commit.md +19 -0
  318. package/src/cli/services/builtin-skills/review-pr.md +21 -0
  319. package/src/cli/services/builtin-skills/review.md +18 -0
@@ -0,0 +1,768 @@
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).
4
+ // All external API keys are fetched via decrypt_secret RPC from encrypted store secrets.
5
+ // ---- Helpers ----
6
+ async function getSecret(sb, name, storeId) {
7
+ const { data, error } = await sb.rpc("decrypt_secret", { p_name: name, p_store_id: storeId });
8
+ if (error || !data)
9
+ return null;
10
+ return data;
11
+ }
12
+ function nowISO() {
13
+ return new Date().toISOString();
14
+ }
15
+ // ---- Main Handler ----
16
+ export async function handleEnrichment(sb, args, storeId) {
17
+ const sid = storeId;
18
+ const action = args.action;
19
+ if (!action) {
20
+ return { success: false, error: "action is required" };
21
+ }
22
+ switch (action) {
23
+ // ---- ENRICH_PERSON: PDL Person Enrichment API ----
24
+ case "enrich_person": {
25
+ const customerId = args.customer_id;
26
+ if (!customerId)
27
+ return { success: false, error: "customer_id is required" };
28
+ const apiKey = await getSecret(sb, "pdl_api_key", sid);
29
+ if (!apiKey)
30
+ return { success: false, error: "PDL API key not configured. Add 'pdl_api_key' to store secrets." };
31
+ // Build PDL query params from available identifiers
32
+ const params = {};
33
+ if (args.email)
34
+ params.email = args.email;
35
+ if (args.phone)
36
+ params.phone = args.phone;
37
+ if (args.first_name)
38
+ params.first_name = args.first_name;
39
+ if (args.last_name)
40
+ params.last_name = args.last_name;
41
+ if (args.linkedin_url)
42
+ params.profile = args.linkedin_url;
43
+ if (args.location)
44
+ params.location = args.location;
45
+ if (Object.keys(params).length === 0) {
46
+ return { success: false, error: "At least one identifier required: email, phone, first_name+last_name, or linkedin_url" };
47
+ }
48
+ try {
49
+ const queryString = new URLSearchParams(params).toString();
50
+ const resp = await fetch(`https://api.peopledatalabs.com/v5/person/enrich?${queryString}`, {
51
+ method: "GET",
52
+ headers: {
53
+ "X-Api-Key": apiKey,
54
+ "Accept": "application/json",
55
+ },
56
+ });
57
+ if (!resp.ok) {
58
+ const text = await resp.text().catch(() => "");
59
+ if (resp.status === 404) {
60
+ return { success: true, data: null, error: "No enrichment data found for this person" };
61
+ }
62
+ return { success: false, error: `PDL API error ${resp.status}: ${text.substring(0, 500)}` };
63
+ }
64
+ const pdlData = await resp.json();
65
+ // Store enrichment profile
66
+ const profile = {
67
+ customer_id: customerId,
68
+ store_id: sid,
69
+ source: "pdl",
70
+ enrichment_data: pdlData,
71
+ enriched_at: nowISO(),
72
+ updated_at: nowISO(),
73
+ };
74
+ // Extract common fields if present
75
+ if (pdlData.full_name)
76
+ profile.full_name = pdlData.full_name;
77
+ if (pdlData.job_title)
78
+ profile.job_title = pdlData.job_title;
79
+ if (pdlData.job_company_name)
80
+ profile.company = pdlData.job_company_name;
81
+ if (pdlData.linkedin_url)
82
+ profile.linkedin_url = pdlData.linkedin_url;
83
+ if (pdlData.location_name)
84
+ profile.location = pdlData.location_name;
85
+ if (pdlData.gender)
86
+ profile.gender = pdlData.gender;
87
+ if (pdlData.birth_year)
88
+ profile.birth_year = pdlData.birth_year;
89
+ // Upsert — update if already exists for this customer+store
90
+ const { data: existing } = await sb.from("customer_enrichment_profiles")
91
+ .select("id")
92
+ .eq("customer_id", customerId)
93
+ .eq("store_id", sid)
94
+ .maybeSingle();
95
+ let result;
96
+ if (existing) {
97
+ const { data, error } = await sb.from("customer_enrichment_profiles")
98
+ .update(profile)
99
+ .eq("id", existing.id)
100
+ .select()
101
+ .single();
102
+ result = { data, error };
103
+ }
104
+ else {
105
+ profile.created_at = nowISO();
106
+ const { data, error } = await sb.from("customer_enrichment_profiles")
107
+ .insert(profile)
108
+ .select()
109
+ .single();
110
+ result = { data, error };
111
+ }
112
+ if (result.error)
113
+ return { success: false, error: `Failed to store enrichment: ${result.error.message}` };
114
+ return { success: true, data: result.data };
115
+ }
116
+ catch (err) {
117
+ const msg = err instanceof Error ? err.message : String(err);
118
+ return { success: false, error: `PDL enrichment failed: ${msg}` };
119
+ }
120
+ }
121
+ // ---- ENRICH_LINKEDIN: Bright Data LinkedIn scraper ----
122
+ case "enrich_linkedin": {
123
+ const customerId = args.customer_id;
124
+ const linkedinUrl = args.linkedin_url;
125
+ if (!customerId)
126
+ return { success: false, error: "customer_id is required" };
127
+ if (!linkedinUrl)
128
+ return { success: false, error: "linkedin_url is required" };
129
+ const apiKey = await getSecret(sb, "brightdata_api_key", sid);
130
+ if (!apiKey)
131
+ return { success: false, error: "Bright Data API key not configured. Add 'brightdata_api_key' to store secrets." };
132
+ const datasetId = await getSecret(sb, "brightdata_dataset_id", sid) || "gd_l1viktl72bvl7bjuj0";
133
+ try {
134
+ // Bright Data Scraper API — synchronous LinkedIn profile collection
135
+ const resp = await fetch(`https://api.brightdata.com/datasets/v3/scrape?dataset_id=${datasetId}&notify=false&include_errors=true`, {
136
+ method: "POST",
137
+ headers: {
138
+ Authorization: `Bearer ${apiKey}`,
139
+ "Content-Type": "application/json",
140
+ },
141
+ body: JSON.stringify({ input: [{ url: linkedinUrl }] }),
142
+ });
143
+ if (!resp.ok) {
144
+ const text = await resp.text().catch(() => "");
145
+ if (resp.status === 404) {
146
+ return { success: true, data: null, error: "No LinkedIn profile found at this URL" };
147
+ }
148
+ return { success: false, error: `Bright Data API error ${resp.status}: ${text.substring(0, 500)}` };
149
+ }
150
+ const bdResult = await resp.json();
151
+ // Bright Data returns an array of results
152
+ const profileData = Array.isArray(bdResult) ? bdResult[0] : bdResult;
153
+ if (!profileData || profileData.error) {
154
+ return { success: true, data: null, error: profileData?.error || "No data returned for this profile" };
155
+ }
156
+ // Store as enrichment profile with source=brightdata
157
+ const profile = {
158
+ customer_id: customerId,
159
+ store_id: sid,
160
+ source: "brightdata",
161
+ enrichment_data: profileData,
162
+ enriched_at: nowISO(),
163
+ updated_at: nowISO(),
164
+ };
165
+ // Extract common fields from Bright Data response
166
+ if (profileData.name || profileData.full_name)
167
+ profile.full_name = profileData.name || profileData.full_name;
168
+ if (profileData.position || profileData.job_title)
169
+ profile.job_title = profileData.position || profileData.job_title;
170
+ if (profileData.current_company || profileData.company)
171
+ profile.company = profileData.current_company || profileData.company;
172
+ if (profileData.headline)
173
+ profile.headline = profileData.headline;
174
+ if (profileData.location)
175
+ profile.location = profileData.location;
176
+ profile.linkedin_url = linkedinUrl;
177
+ // Upsert — update if already exists for this customer+store+source
178
+ const { data: existing } = await sb.from("customer_enrichment_profiles")
179
+ .select("id")
180
+ .eq("customer_id", customerId)
181
+ .eq("store_id", sid)
182
+ .eq("source", "brightdata")
183
+ .maybeSingle();
184
+ let result;
185
+ if (existing) {
186
+ const { data, error } = await sb.from("customer_enrichment_profiles")
187
+ .update(profile)
188
+ .eq("id", existing.id)
189
+ .select()
190
+ .single();
191
+ result = { data, error };
192
+ }
193
+ else {
194
+ profile.created_at = nowISO();
195
+ const { data, error } = await sb.from("customer_enrichment_profiles")
196
+ .insert(profile)
197
+ .select()
198
+ .single();
199
+ result = { data, error };
200
+ }
201
+ if (result.error)
202
+ return { success: false, error: `Failed to store enrichment: ${result.error.message}` };
203
+ return { success: true, data: result.data };
204
+ }
205
+ catch (err) {
206
+ const msg = err instanceof Error ? err.message : String(err);
207
+ return { success: false, error: `Bright Data enrichment failed: ${msg}` };
208
+ }
209
+ }
210
+ // ---- CHECK_XONPLUS: xonPlus real-time breach monitoring ----
211
+ case "check_xonplus": {
212
+ const customerId = args.customer_id;
213
+ const email = args.email;
214
+ if (!customerId)
215
+ return { success: false, error: "customer_id is required" };
216
+ if (!email)
217
+ return { success: false, error: "email is required" };
218
+ const apiKey = await getSecret(sb, "xonplus_api_key", sid);
219
+ if (!apiKey)
220
+ return { success: false, error: "xonPlus API key not configured. Add 'xonplus_api_key' to store secrets." };
221
+ try {
222
+ const resp = await fetch(`https://api.xposedornot.com/v1/check-email/${encodeURIComponent(email)}`, {
223
+ method: "GET",
224
+ headers: {
225
+ "x-api-key": apiKey,
226
+ Accept: "application/json",
227
+ },
228
+ });
229
+ if (!resp.ok) {
230
+ const text = await resp.text().catch(() => "");
231
+ if (resp.status === 404) {
232
+ return { success: true, data: { email, total_breaches: 0, breaches: [] } };
233
+ }
234
+ return { success: false, error: `xonPlus API error ${resp.status}: ${text.substring(0, 500)}` };
235
+ }
236
+ const xonData = await resp.json();
237
+ const breaches = (xonData.breaches || xonData.ExposedBreaches?.breaches_details || []);
238
+ // Store each breach record
239
+ const inserted = [];
240
+ for (const breach of breaches) {
241
+ const record = {
242
+ customer_id: customerId,
243
+ store_id: sid,
244
+ breach_name: breach.breach || breach.name || "unknown",
245
+ breach_domain: breach.domain || null,
246
+ breach_date: breach.xposed_date || breach.date || null,
247
+ data_classes: breach.xposed_data ? breach.xposed_data.split(",").map((s) => s.trim()) : null,
248
+ source: "xonplus",
249
+ raw_data: breach,
250
+ discovered_at: nowISO(),
251
+ created_at: nowISO(),
252
+ };
253
+ // Deduplicate by customer_id + breach_name + source
254
+ const { data: existing } = await sb.from("customer_breach_records")
255
+ .select("id")
256
+ .eq("customer_id", customerId)
257
+ .eq("breach_name", record.breach_name)
258
+ .eq("source", "xonplus")
259
+ .maybeSingle();
260
+ if (!existing) {
261
+ const { data, error } = await sb.from("customer_breach_records")
262
+ .insert(record)
263
+ .select()
264
+ .single();
265
+ if (!error && data)
266
+ inserted.push(data);
267
+ }
268
+ }
269
+ return {
270
+ success: true,
271
+ data: {
272
+ email,
273
+ total_breaches: breaches.length,
274
+ new_breaches: inserted.length,
275
+ risk_metrics: xonData.BreachMetrics || null,
276
+ breaches: inserted.length > 0 ? inserted : breaches,
277
+ },
278
+ };
279
+ }
280
+ catch (err) {
281
+ const msg = err instanceof Error ? err.message : String(err);
282
+ return { success: false, error: `xonPlus breach check failed: ${msg}` };
283
+ }
284
+ }
285
+ // ---- CHECK_BREACHES: HIBP Breach Check ----
286
+ case "check_breaches": {
287
+ const customerId = args.customer_id;
288
+ const email = args.email;
289
+ if (!customerId)
290
+ return { success: false, error: "customer_id is required" };
291
+ if (!email)
292
+ return { success: false, error: "email is required" };
293
+ const apiKey = await getSecret(sb, "hibp_api_key", sid);
294
+ if (!apiKey)
295
+ return { success: false, error: "HIBP API key not configured. Add 'hibp_api_key' to store secrets." };
296
+ try {
297
+ const resp = await fetch(`https://haveibeenpwned.com/api/v3/breachedaccount/${encodeURIComponent(email)}?truncateResponse=false`, {
298
+ method: "GET",
299
+ headers: {
300
+ "hibp-api-key": apiKey,
301
+ "User-Agent": "SwagManager-DataProtection",
302
+ Accept: "application/json",
303
+ },
304
+ });
305
+ let breaches = [];
306
+ if (resp.status === 404) {
307
+ // No breaches found — that's good
308
+ breaches = [];
309
+ }
310
+ else if (!resp.ok) {
311
+ const text = await resp.text().catch(() => "");
312
+ return { success: false, error: `HIBP API error ${resp.status}: ${text.substring(0, 500)}` };
313
+ }
314
+ else {
315
+ breaches = await resp.json();
316
+ }
317
+ // Store each breach record
318
+ const inserted = [];
319
+ for (const breach of breaches) {
320
+ const record = {
321
+ customer_id: customerId,
322
+ store_id: sid,
323
+ breach_name: breach.Name,
324
+ breach_domain: breach.Domain,
325
+ breach_date: breach.BreachDate,
326
+ data_classes: breach.DataClasses,
327
+ description: breach.Description,
328
+ is_verified: breach.IsVerified,
329
+ is_sensitive: breach.IsSensitive,
330
+ source: "hibp",
331
+ discovered_at: nowISO(),
332
+ created_at: nowISO(),
333
+ };
334
+ // Upsert by customer_id + breach_name to avoid duplicates
335
+ const { data: existing } = await sb.from("customer_breach_records")
336
+ .select("id")
337
+ .eq("customer_id", customerId)
338
+ .eq("breach_name", breach.Name)
339
+ .maybeSingle();
340
+ if (!existing) {
341
+ const { data, error } = await sb.from("customer_breach_records")
342
+ .insert(record)
343
+ .select()
344
+ .single();
345
+ if (!error && data)
346
+ inserted.push(data);
347
+ }
348
+ }
349
+ return {
350
+ success: true,
351
+ data: {
352
+ email,
353
+ total_breaches: breaches.length,
354
+ new_breaches: inserted.length,
355
+ breaches: inserted.length > 0 ? inserted : breaches,
356
+ },
357
+ };
358
+ }
359
+ catch (err) {
360
+ const msg = err instanceof Error ? err.message : String(err);
361
+ return { success: false, error: `HIBP breach check failed: ${msg}` };
362
+ }
363
+ }
364
+ // ---- CHECK_DEHASHED: DeHashed credential exposure search ----
365
+ case "check_dehashed": {
366
+ const customerId = args.customer_id;
367
+ const email = args.email;
368
+ if (!customerId)
369
+ return { success: false, error: "customer_id is required" };
370
+ if (!email)
371
+ return { success: false, error: "email is required" };
372
+ const apiKey = await getSecret(sb, "dehashed_api_key", sid);
373
+ if (!apiKey)
374
+ return { success: false, error: "DeHashed API key not configured. Add 'dehashed_api_key' to store secrets." };
375
+ try {
376
+ const resp = await fetch("https://api.dehashed.com/v2/search", {
377
+ method: "POST",
378
+ headers: {
379
+ "Content-Type": "application/json",
380
+ "DeHashed-Api-Key": apiKey,
381
+ },
382
+ body: JSON.stringify({
383
+ query: `email:"${email}"`,
384
+ page: 1,
385
+ size: 100,
386
+ wildcard: false,
387
+ regex: false,
388
+ de_dupe: true,
389
+ }),
390
+ });
391
+ if (!resp.ok) {
392
+ const text = await resp.text().catch(() => "");
393
+ return { success: false, error: `DeHashed API error ${resp.status}: ${text.substring(0, 500)}` };
394
+ }
395
+ const result = await resp.json();
396
+ const entries = (result.entries || []);
397
+ // Store each as a breach record with source=dehashed
398
+ const inserted = [];
399
+ for (const entry of entries) {
400
+ const record = {
401
+ customer_id: customerId,
402
+ store_id: sid,
403
+ breach_name: entry.database_name || "unknown",
404
+ breach_domain: entry.domain || null,
405
+ breach_date: entry.obtained_date || null,
406
+ data_classes: entry.type ? [entry.type] : null,
407
+ source: "dehashed",
408
+ raw_data: entry,
409
+ discovered_at: nowISO(),
410
+ created_at: nowISO(),
411
+ };
412
+ const { data, error } = await sb.from("customer_breach_records")
413
+ .insert(record)
414
+ .select()
415
+ .single();
416
+ if (!error && data)
417
+ inserted.push(data);
418
+ }
419
+ return {
420
+ success: true,
421
+ data: {
422
+ email,
423
+ total_results: entries.length,
424
+ stored: inserted.length,
425
+ entries: inserted,
426
+ },
427
+ };
428
+ }
429
+ catch (err) {
430
+ const msg = err instanceof Error ? err.message : String(err);
431
+ return { success: false, error: `DeHashed check failed: ${msg}` };
432
+ }
433
+ }
434
+ // ---- GET_ENRICHMENT: Read enrichment profiles ----
435
+ case "get_enrichment": {
436
+ const customerId = args.customer_id;
437
+ if (!customerId)
438
+ return { success: false, error: "customer_id is required" };
439
+ let q = sb.from("customer_enrichment_profiles")
440
+ .select("*")
441
+ .eq("customer_id", customerId)
442
+ .eq("store_id", sid)
443
+ .order("enriched_at", { ascending: false });
444
+ if (args.source)
445
+ q = q.eq("source", args.source);
446
+ const limit = args.limit || 10;
447
+ q = q.limit(limit);
448
+ const { data, error } = await q;
449
+ return error
450
+ ? { success: false, error: error.message }
451
+ : { success: true, data: { count: data?.length, records: data } };
452
+ }
453
+ // ---- GET_BREACHES: Read breach records ----
454
+ case "get_breaches": {
455
+ const customerId = args.customer_id;
456
+ if (!customerId)
457
+ return { success: false, error: "customer_id is required" };
458
+ let q = sb.from("customer_breach_records")
459
+ .select("*")
460
+ .eq("customer_id", customerId)
461
+ .eq("store_id", sid)
462
+ .order("discovered_at", { ascending: false });
463
+ if (args.source)
464
+ q = q.eq("source", args.source);
465
+ const limit = args.limit || 50;
466
+ q = q.limit(limit);
467
+ const { data, error } = await q;
468
+ return error
469
+ ? { success: false, error: error.message }
470
+ : { success: true, data: { count: data?.length, records: data } };
471
+ }
472
+ // ---- GET_EXPOSURES: Read broker exposures ----
473
+ case "get_exposures": {
474
+ const customerId = args.customer_id;
475
+ if (!customerId)
476
+ return { success: false, error: "customer_id is required" };
477
+ let q = sb.from("customer_exposures")
478
+ .select("*")
479
+ .eq("customer_id", customerId)
480
+ .eq("store_id", sid)
481
+ .order("discovered_at", { ascending: false });
482
+ if (args.status)
483
+ q = q.eq("status", args.status);
484
+ if (args.broker)
485
+ q = q.eq("broker", args.broker);
486
+ const limit = args.limit || 50;
487
+ q = q.limit(limit);
488
+ const { data, error } = await q;
489
+ return error
490
+ ? { success: false, error: error.message }
491
+ : { success: true, data: { count: data?.length, records: data } };
492
+ }
493
+ // ---- GET_REMOVAL_STATUS: Read removal requests ----
494
+ case "get_removal_status": {
495
+ const customerId = args.customer_id;
496
+ if (!customerId)
497
+ return { success: false, error: "customer_id is required" };
498
+ let q = sb.from("customer_removal_requests")
499
+ .select("*")
500
+ .eq("customer_id", customerId)
501
+ .eq("store_id", sid)
502
+ .order("created_at", { ascending: false });
503
+ if (args.status)
504
+ q = q.eq("status", args.status);
505
+ const limit = args.limit || 50;
506
+ q = q.limit(limit);
507
+ const { data, error } = await q;
508
+ return error
509
+ ? { success: false, error: error.message }
510
+ : { success: true, data: { count: data?.length, records: data } };
511
+ }
512
+ // ---- GET_RISK_SCORE: Read latest risk score ----
513
+ case "get_risk_score": {
514
+ const customerId = args.customer_id;
515
+ if (!customerId)
516
+ return { success: false, error: "customer_id is required" };
517
+ const { data, error } = await sb.from("customer_risk_scores")
518
+ .select("*")
519
+ .eq("customer_id", customerId)
520
+ .eq("store_id", sid)
521
+ .order("calculated_at", { ascending: false })
522
+ .limit(1)
523
+ .maybeSingle();
524
+ return error
525
+ ? { success: false, error: error.message }
526
+ : { success: true, data };
527
+ }
528
+ // ---- STORE_SCAN_RESULTS: Insert scan results ----
529
+ case "store_scan_results": {
530
+ const customerId = args.customer_id;
531
+ if (!customerId)
532
+ return { success: false, error: "customer_id is required" };
533
+ const record = {
534
+ customer_id: customerId,
535
+ store_id: sid,
536
+ scan_type: args.scan_type || "discovery",
537
+ scan_data: args.scan_data || {},
538
+ broker_count: args.broker_count ?? 0,
539
+ exposure_count: args.exposure_count ?? 0,
540
+ status: args.status || "completed",
541
+ created_at: nowISO(),
542
+ };
543
+ if (args.scan_id)
544
+ record.scan_id = args.scan_id;
545
+ if (args.file_path)
546
+ record.file_path = args.file_path;
547
+ const { data, error } = await sb.from("customer_scan_results")
548
+ .insert(record)
549
+ .select()
550
+ .single();
551
+ return error
552
+ ? { success: false, error: `Failed to store scan results: ${error.message}` }
553
+ : { success: true, data };
554
+ }
555
+ // ---- STORE_EXPOSURE: Insert a broker exposure ----
556
+ case "store_exposure": {
557
+ const customerId = args.customer_id;
558
+ const broker = args.broker;
559
+ if (!customerId)
560
+ return { success: false, error: "customer_id is required" };
561
+ if (!broker)
562
+ return { success: false, error: "broker is required" };
563
+ // Check for existing exposure to avoid duplicates
564
+ const { data: existing } = await sb.from("customer_exposures")
565
+ .select("id")
566
+ .eq("customer_id", customerId)
567
+ .eq("store_id", sid)
568
+ .eq("broker", broker)
569
+ .maybeSingle();
570
+ if (existing) {
571
+ // Update existing record
572
+ const updates = { updated_at: nowISO() };
573
+ if (args.profile_url)
574
+ updates.profile_url = args.profile_url;
575
+ if (args.data_found)
576
+ updates.data_found = args.data_found;
577
+ if (args.status)
578
+ updates.status = args.status;
579
+ if (args.exposure_data)
580
+ updates.exposure_data = args.exposure_data;
581
+ const { data, error } = await sb.from("customer_exposures")
582
+ .update(updates)
583
+ .eq("id", existing.id)
584
+ .select()
585
+ .single();
586
+ return error
587
+ ? { success: false, error: `Failed to update exposure: ${error.message}` }
588
+ : { success: true, data: { ...data, _note: "Updated existing exposure record" } };
589
+ }
590
+ const record = {
591
+ customer_id: customerId,
592
+ store_id: sid,
593
+ broker,
594
+ status: args.status || "found",
595
+ discovered_at: nowISO(),
596
+ created_at: nowISO(),
597
+ };
598
+ if (args.profile_url)
599
+ record.profile_url = args.profile_url;
600
+ if (args.data_found)
601
+ record.data_found = args.data_found;
602
+ if (args.exposure_data)
603
+ record.exposure_data = args.exposure_data;
604
+ if (args.scan_id)
605
+ record.scan_id = args.scan_id;
606
+ const { data, error } = await sb.from("customer_exposures")
607
+ .insert(record)
608
+ .select()
609
+ .single();
610
+ return error
611
+ ? { success: false, error: `Failed to store exposure: ${error.message}` }
612
+ : { success: true, data };
613
+ }
614
+ // ---- UPDATE_EXPOSURE_STATUS: Update exposure status ----
615
+ case "update_exposure_status": {
616
+ const exposureId = args.exposure_id;
617
+ if (!exposureId)
618
+ return { success: false, error: "exposure_id is required" };
619
+ const updates = { updated_at: nowISO() };
620
+ if (args.status)
621
+ updates.status = args.status;
622
+ if (args.removal_method)
623
+ updates.removal_method = args.removal_method;
624
+ if (args.removal_confirmed_at)
625
+ updates.removal_confirmed_at = args.removal_confirmed_at;
626
+ if (args.notes)
627
+ updates.notes = args.notes;
628
+ if (args.status === "removed") {
629
+ updates.removed_at = nowISO();
630
+ }
631
+ const { data, error } = await sb.from("customer_exposures")
632
+ .update(updates)
633
+ .eq("id", exposureId)
634
+ .eq("store_id", sid)
635
+ .select()
636
+ .single();
637
+ return error
638
+ ? { success: false, error: `Failed to update exposure: ${error.message}` }
639
+ : { success: true, data };
640
+ }
641
+ // ---- STORE_REMOVAL_REQUEST: Insert a removal request ----
642
+ case "store_removal_request": {
643
+ const customerId = args.customer_id;
644
+ const broker = args.broker;
645
+ if (!customerId)
646
+ return { success: false, error: "customer_id is required" };
647
+ if (!broker)
648
+ return { success: false, error: "broker is required" };
649
+ const record = {
650
+ customer_id: customerId,
651
+ store_id: sid,
652
+ broker,
653
+ method: args.method || "manual",
654
+ status: args.status || "pending",
655
+ created_at: nowISO(),
656
+ updated_at: nowISO(),
657
+ };
658
+ if (args.exposure_id)
659
+ record.exposure_id = args.exposure_id;
660
+ if (args.request_data)
661
+ record.request_data = args.request_data;
662
+ if (args.submitted_at)
663
+ record.submitted_at = args.submitted_at;
664
+ if (args.confirmation_id)
665
+ record.confirmation_id = args.confirmation_id;
666
+ const { data, error } = await sb.from("customer_removal_requests")
667
+ .insert(record)
668
+ .select()
669
+ .single();
670
+ return error
671
+ ? { success: false, error: `Failed to store removal request: ${error.message}` }
672
+ : { success: true, data };
673
+ }
674
+ // ---- CALCULATE_RISK_SCORE: Compute and store a risk score ----
675
+ case "calculate_risk_score": {
676
+ const customerId = args.customer_id;
677
+ if (!customerId)
678
+ return { success: false, error: "customer_id is required" };
679
+ // Gather data for risk calculation
680
+ const [exposuresResult, breachesResult, removalsResult] = await Promise.all([
681
+ sb.from("customer_exposures")
682
+ .select("id, broker, status, data_found")
683
+ .eq("customer_id", customerId)
684
+ .eq("store_id", sid),
685
+ sb.from("customer_breach_records")
686
+ .select("id, breach_name, data_classes, is_sensitive, is_verified")
687
+ .eq("customer_id", customerId)
688
+ .eq("store_id", sid),
689
+ sb.from("customer_removal_requests")
690
+ .select("id, status, broker")
691
+ .eq("customer_id", customerId)
692
+ .eq("store_id", sid),
693
+ ]);
694
+ const exposures = exposuresResult.data || [];
695
+ const breaches = breachesResult.data || [];
696
+ const removals = removalsResult.data || [];
697
+ // Score calculation:
698
+ // - Each active exposure: +10 points
699
+ // - Each removed exposure: -5 points (still contributes slightly)
700
+ // - Each breach: +15 points
701
+ // - Each sensitive breach: +25 points (instead of 15)
702
+ // - Each verified breach: +5 bonus
703
+ // - Each completed removal: -8 points
704
+ // - Base floor: 0, cap: 100
705
+ let score = 0;
706
+ // Exposure scoring
707
+ const activeExposures = exposures.filter((e) => e.status !== "removed");
708
+ const removedExposures = exposures.filter((e) => e.status === "removed");
709
+ score += activeExposures.length * 10;
710
+ score += removedExposures.length * 2;
711
+ // Breach scoring
712
+ for (const breach of breaches) {
713
+ if (breach.is_sensitive) {
714
+ score += 25;
715
+ }
716
+ else {
717
+ score += 15;
718
+ }
719
+ if (breach.is_verified) {
720
+ score += 5;
721
+ }
722
+ }
723
+ // Removal credit
724
+ const completedRemovals = removals.filter((r) => r.status === "completed" || r.status === "confirmed");
725
+ score -= completedRemovals.length * 8;
726
+ // Clamp to 0-100
727
+ score = Math.max(0, Math.min(100, score));
728
+ // Determine risk level
729
+ let riskLevel;
730
+ if (score >= 75)
731
+ riskLevel = "critical";
732
+ else if (score >= 50)
733
+ riskLevel = "high";
734
+ else if (score >= 25)
735
+ riskLevel = "medium";
736
+ else
737
+ riskLevel = "low";
738
+ const riskRecord = {
739
+ customer_id: customerId,
740
+ store_id: sid,
741
+ overall_score: score,
742
+ risk_level: riskLevel,
743
+ factors: {
744
+ active_exposures: activeExposures.length,
745
+ removed_exposures: removedExposures.length,
746
+ total_breaches: breaches.length,
747
+ sensitive_breaches: breaches.filter((b) => b.is_sensitive).length,
748
+ completed_removals: completedRemovals.length,
749
+ pending_removals: removals.filter((r) => r.status === "pending").length,
750
+ },
751
+ calculated_at: nowISO(),
752
+ created_at: nowISO(),
753
+ };
754
+ const { data, error } = await sb.from("customer_risk_scores")
755
+ .insert(riskRecord)
756
+ .select()
757
+ .single();
758
+ return error
759
+ ? { success: false, error: `Failed to store risk score: ${error.message}` }
760
+ : { success: true, data };
761
+ }
762
+ default:
763
+ return {
764
+ success: false,
765
+ 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`,
766
+ };
767
+ }
768
+ }