kontext-sdk 0.2.0 → 0.2.5

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/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var crypto$1 = require('crypto');
4
- var fs2 = require('fs');
5
- var path2 = require('path');
4
+ var fs3 = require('fs');
5
+ var path3 = require('path');
6
6
 
7
7
  function _interopNamespace(e) {
8
8
  if (e && e.__esModule) return e;
@@ -22,8 +22,8 @@ function _interopNamespace(e) {
22
22
  return Object.freeze(n);
23
23
  }
24
24
 
25
- var fs2__namespace = /*#__PURE__*/_interopNamespace(fs2);
26
- var path2__namespace = /*#__PURE__*/_interopNamespace(path2);
25
+ var fs3__namespace = /*#__PURE__*/_interopNamespace(fs3);
26
+ var path3__namespace = /*#__PURE__*/_interopNamespace(path3);
27
27
 
28
28
  // src/types.ts
29
29
  var KontextErrorCode = /* @__PURE__ */ ((KontextErrorCode2) => {
@@ -51,6 +51,8 @@ var KontextError = class extends Error {
51
51
  };
52
52
 
53
53
  // src/store.ts
54
+ var DEFAULT_MAX_ENTRIES = 1e4;
55
+ var EVICTION_RATIO = 0.1;
54
56
  var STORAGE_KEYS = {
55
57
  actions: "kontext:actions",
56
58
  transactions: "kontext:transactions",
@@ -63,6 +65,10 @@ var KontextStore = class {
63
65
  tasks = /* @__PURE__ */ new Map();
64
66
  anomalies = [];
65
67
  storageAdapter = null;
68
+ maxEntries;
69
+ constructor(maxEntries = DEFAULT_MAX_ENTRIES) {
70
+ this.maxEntries = maxEntries;
71
+ }
66
72
  /**
67
73
  * Attach a storage adapter for persistence.
68
74
  *
@@ -86,14 +92,15 @@ var KontextStore = class {
86
92
  */
87
93
  async flush() {
88
94
  if (!this.storageAdapter) return;
95
+ const actionsSnapshot = [...this.actions];
96
+ const transactionsSnapshot = [...this.transactions];
97
+ const tasksSnapshot = Array.from(this.tasks.entries());
98
+ const anomaliesSnapshot = [...this.anomalies];
89
99
  await Promise.all([
90
- this.storageAdapter.save(STORAGE_KEYS.actions, this.actions),
91
- this.storageAdapter.save(STORAGE_KEYS.transactions, this.transactions),
92
- this.storageAdapter.save(
93
- STORAGE_KEYS.tasks,
94
- Array.from(this.tasks.entries())
95
- ),
96
- this.storageAdapter.save(STORAGE_KEYS.anomalies, this.anomalies)
100
+ this.storageAdapter.save(STORAGE_KEYS.actions, actionsSnapshot),
101
+ this.storageAdapter.save(STORAGE_KEYS.transactions, transactionsSnapshot),
102
+ this.storageAdapter.save(STORAGE_KEYS.tasks, tasksSnapshot),
103
+ this.storageAdapter.save(STORAGE_KEYS.anomalies, anomaliesSnapshot)
97
104
  ]);
98
105
  }
99
106
  /**
@@ -125,9 +132,12 @@ var KontextStore = class {
125
132
  // --------------------------------------------------------------------------
126
133
  // Actions
127
134
  // --------------------------------------------------------------------------
128
- /** Append an action log entry. */
135
+ /** Append an action log entry. Evicts oldest 10% when maxEntries is exceeded. */
129
136
  addAction(action) {
130
137
  this.actions.push(action);
138
+ if (this.actions.length > this.maxEntries) {
139
+ this.actions.splice(0, Math.ceil(this.maxEntries * EVICTION_RATIO));
140
+ }
131
141
  }
132
142
  /** Retrieve all action log entries. */
133
143
  getActions() {
@@ -144,9 +154,12 @@ var KontextStore = class {
144
154
  // --------------------------------------------------------------------------
145
155
  // Transactions
146
156
  // --------------------------------------------------------------------------
147
- /** Append a transaction record. */
157
+ /** Append a transaction record. Evicts oldest 10% when maxEntries is exceeded. */
148
158
  addTransaction(tx) {
149
159
  this.transactions.push(tx);
160
+ if (this.transactions.length > this.maxEntries) {
161
+ this.transactions.splice(0, Math.ceil(this.maxEntries * EVICTION_RATIO));
162
+ }
150
163
  }
151
164
  /** Retrieve all transaction records. */
152
165
  getTransactions() {
@@ -194,9 +207,12 @@ var KontextStore = class {
194
207
  // --------------------------------------------------------------------------
195
208
  // Anomalies
196
209
  // --------------------------------------------------------------------------
197
- /** Append an anomaly event. */
210
+ /** Append an anomaly event. Evicts oldest 10% when maxEntries is exceeded. */
198
211
  addAnomaly(anomaly) {
199
212
  this.anomalies.push(anomaly);
213
+ if (this.anomalies.length > this.maxEntries) {
214
+ this.anomalies.splice(0, Math.ceil(this.maxEntries * EVICTION_RATIO));
215
+ }
200
216
  }
201
217
  /** Retrieve all anomaly events. */
202
218
  getAnomalies() {
@@ -397,7 +413,8 @@ var DigestChain = class {
397
413
  */
398
414
  getPrecisionTimestamp() {
399
415
  const hrtime = process.hrtime.bigint();
400
- const microseconds = Number((hrtime - this.hrtimeBase) % 1000000n);
416
+ const delta = hrtime >= this.hrtimeBase ? hrtime - this.hrtimeBase : 0n;
417
+ const microseconds = Number(delta % 1000000n);
401
418
  return {
402
419
  iso: (/* @__PURE__ */ new Date()).toISOString(),
403
420
  hrtime,
@@ -492,6 +509,12 @@ function toCsv(records) {
492
509
  function clamp(value, min, max) {
493
510
  return Math.min(Math.max(value, min), max);
494
511
  }
512
+ var LOG_LEVEL_SEVERITY = {
513
+ debug: 0,
514
+ info: 1,
515
+ warn: 2,
516
+ error: 3
517
+ };
495
518
  var ActionLogger = class {
496
519
  config;
497
520
  store;
@@ -501,6 +524,7 @@ var ActionLogger = class {
501
524
  batchSize;
502
525
  flushIntervalMs;
503
526
  isCloudMode;
527
+ logLevel;
504
528
  constructor(config, store) {
505
529
  this.config = config;
506
530
  this.store = store;
@@ -508,6 +532,7 @@ var ActionLogger = class {
508
532
  this.batchSize = config.batchSize ?? 50;
509
533
  this.flushIntervalMs = config.flushIntervalMs ?? 5e3;
510
534
  this.isCloudMode = !!config.apiKey;
535
+ this.logLevel = config.logLevel ?? (config.debug ? "debug" : "warn");
511
536
  this.flushTimer = setInterval(() => {
512
537
  void this.flush();
513
538
  }, this.flushIntervalMs);
@@ -550,9 +575,7 @@ var ActionLogger = class {
550
575
  if (this.batch.length >= this.batchSize) {
551
576
  await this.flush();
552
577
  }
553
- if (this.config.debug) {
554
- this.debugLog("Action logged", action);
555
- }
578
+ this.emitLog("debug", "Action logged", action);
556
579
  return action;
557
580
  }
558
581
  /**
@@ -604,9 +627,7 @@ var ActionLogger = class {
604
627
  if (this.batch.length >= this.batchSize) {
605
628
  await this.flush();
606
629
  }
607
- if (this.config.debug) {
608
- this.debugLog("Transaction logged", record);
609
- }
630
+ this.emitLog("debug", "Transaction logged", record);
610
631
  return record;
611
632
  }
612
633
  /**
@@ -708,21 +729,19 @@ var ActionLogger = class {
708
729
  }
709
730
  flushToFile(actions) {
710
731
  const outputDir = this.config.localOutputDir ?? ".kontext";
711
- const logDir = path2__namespace.join(outputDir, "logs");
732
+ const logDir = path3__namespace.join(outputDir, "logs");
712
733
  try {
713
- fs2__namespace.mkdirSync(logDir, { recursive: true });
734
+ fs3__namespace.mkdirSync(logDir, { recursive: true });
714
735
  const filename = `actions-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.jsonl`;
715
- const filePath = path2__namespace.join(logDir, filename);
736
+ const filePath = path3__namespace.join(logDir, filename);
716
737
  const lines = actions.map((a) => JSON.stringify(a)).join("\n") + "\n";
717
- fs2__namespace.appendFileSync(filePath, lines, "utf-8");
738
+ fs3__namespace.appendFileSync(filePath, lines, "utf-8");
718
739
  } catch (error) {
719
- if (this.config.debug) {
720
- this.debugLog("Failed to write log file", { error });
721
- }
740
+ this.emitLog("warn", "Failed to write log file", { error });
722
741
  }
723
742
  }
724
743
  async flushToApi(actions) {
725
- const apiUrl = this.config.apiUrl ?? "https://kontext-api-421314897784.us-central1.run.app";
744
+ const apiUrl = this.config.apiUrl ?? process.env["KONTEXT_API_URL"] ?? "https://api.kontext.so";
726
745
  try {
727
746
  const response = await fetch(`${apiUrl}/v1/actions`, {
728
747
  method: "POST",
@@ -742,15 +761,35 @@ var ActionLogger = class {
742
761
  }
743
762
  } catch (error) {
744
763
  if (error instanceof KontextError) throw error;
745
- if (this.config.debug) {
746
- this.debugLog("API flush failed, falling back to local file", { error });
747
- }
764
+ this.emitLog("warn", "API flush failed, falling back to local file", { error });
748
765
  this.flushToFile(actions);
749
766
  }
750
767
  }
751
- debugLog(message, data) {
768
+ /**
769
+ * Emit a log message at the specified severity level.
770
+ * Only outputs if the message level meets or exceeds the configured logLevel.
771
+ */
772
+ emitLog(level, message, data) {
773
+ if (LOG_LEVEL_SEVERITY[level] < LOG_LEVEL_SEVERITY[this.logLevel]) {
774
+ return;
775
+ }
752
776
  const timestamp = now();
753
- console.debug(`[Kontext ${timestamp}] ${message}`, data ? JSON.stringify(data, null, 2) : "");
777
+ const formatted = `[Kontext ${timestamp}] ${message}`;
778
+ const payload = data ? JSON.stringify(data, null, 2) : "";
779
+ switch (level) {
780
+ case "debug":
781
+ console.debug(formatted, payload);
782
+ break;
783
+ case "info":
784
+ console.info(formatted, payload);
785
+ break;
786
+ case "warn":
787
+ console.warn(formatted, payload);
788
+ break;
789
+ case "error":
790
+ console.error(formatted, payload);
791
+ break;
792
+ }
754
793
  }
755
794
  };
756
795
 
@@ -1478,6 +1517,18 @@ var AuditExporter = class {
1478
1517
  };
1479
1518
 
1480
1519
  // src/trust.ts
1520
+ var TRUST_LEVEL_VERIFIED = 90;
1521
+ var TRUST_LEVEL_HIGH = 70;
1522
+ var TRUST_LEVEL_MEDIUM = 50;
1523
+ var TRUST_LEVEL_LOW = 30;
1524
+ var RISK_FLAG_THRESHOLD = 60;
1525
+ var RISK_BLOCK_THRESHOLD = 80;
1526
+ var RISK_REVIEW_THRESHOLD = 50;
1527
+ var WEIGHT_HISTORY = 0.15;
1528
+ var WEIGHT_TASK_COMPLETION = 0.25;
1529
+ var WEIGHT_ANOMALY = 0.25;
1530
+ var WEIGHT_TX_CONSISTENCY = 0.2;
1531
+ var WEIGHT_COMPLIANCE = 0.15;
1481
1532
  var TrustScorer = class {
1482
1533
  config;
1483
1534
  store;
@@ -1536,8 +1587,8 @@ var TrustScorer = class {
1536
1587
  const totalScore = factors.reduce((sum, f) => sum + f.score, 0);
1537
1588
  const riskScore = clamp(Math.round(totalScore / Math.max(factors.length, 1)), 0, 100);
1538
1589
  const riskLevel = this.riskScoreToLevel(riskScore);
1539
- const flagged = riskScore >= 60;
1540
- const recommendation = riskScore >= 80 ? "block" : riskScore >= 50 ? "review" : "approve";
1590
+ const flagged = riskScore >= RISK_FLAG_THRESHOLD;
1591
+ const recommendation = riskScore >= RISK_BLOCK_THRESHOLD ? "block" : riskScore >= RISK_REVIEW_THRESHOLD ? "review" : "approve";
1541
1592
  return {
1542
1593
  txHash: tx.txHash,
1543
1594
  riskScore,
@@ -1552,17 +1603,34 @@ var TrustScorer = class {
1552
1603
  // Agent trust factor computation
1553
1604
  // --------------------------------------------------------------------------
1554
1605
  computeAgentFactors(agentId) {
1555
- const factors = [];
1556
- factors.push(this.computeHistoryDepthFactor(agentId));
1557
- factors.push(this.computeTaskCompletionFactor(agentId));
1558
- factors.push(this.computeAnomalyFrequencyFactor(agentId));
1559
- factors.push(this.computeTransactionConsistencyFactor(agentId));
1560
- factors.push(this.computeComplianceAdherenceFactor(agentId));
1561
- return factors;
1606
+ const data = {
1607
+ actions: this.store.getActionsByAgent(agentId),
1608
+ transactions: this.store.getTransactionsByAgent(agentId),
1609
+ tasks: this.store.queryTasks((t) => t.agentId === agentId),
1610
+ anomalies: this.store.queryAnomalies((a) => a.agentId === agentId)
1611
+ };
1612
+ return [
1613
+ this.computeHistoryDepthFactor(data),
1614
+ this.computeTaskCompletionFactor(data),
1615
+ this.computeAnomalyFrequencyFactor(data),
1616
+ this.computeTransactionConsistencyFactor(data),
1617
+ this.computeComplianceAdherenceFactor(data)
1618
+ ];
1562
1619
  }
1563
- computeHistoryDepthFactor(agentId) {
1564
- const actions = this.store.getActionsByAgent(agentId);
1565
- const count = actions.length;
1620
+ /**
1621
+ * History depth factor: more recorded actions = more data to assess trust.
1622
+ *
1623
+ * Scoring curve (step function, not linear) prevents gaming by spamming
1624
+ * low-value actions — crossing each tier requires meaningfully more history:
1625
+ * - 0 actions → 10 (minimal trust, new agent)
1626
+ * - 1-4 → 30 (some activity but too little to draw conclusions)
1627
+ * - 5-19 → 50 (neutral, moderate activity)
1628
+ * - 20-49 → 70 (established agent with reasonable track record)
1629
+ * - 50-99 → 85 (well-established agent)
1630
+ * - 100+ → 95 (capped below 100 because history alone doesn't guarantee trust)
1631
+ */
1632
+ computeHistoryDepthFactor(data) {
1633
+ const count = data.actions.length;
1566
1634
  let score;
1567
1635
  if (count === 0) score = 10;
1568
1636
  else if (count < 5) score = 30;
@@ -1573,44 +1641,65 @@ var TrustScorer = class {
1573
1641
  return {
1574
1642
  name: "history_depth",
1575
1643
  score,
1576
- weight: 0.15,
1644
+ weight: WEIGHT_HISTORY,
1577
1645
  description: `Agent has ${count} recorded actions`
1578
1646
  };
1579
1647
  }
1580
- computeTaskCompletionFactor(agentId) {
1581
- const tasks = this.store.queryTasks((t) => t.agentId === agentId);
1582
- const totalTasks = tasks.length;
1648
+ /**
1649
+ * Task completion factor: agents that confirm tasks build trust, failures erode it.
1650
+ *
1651
+ * Formula: score = (completionRate * 100) - (failureRate * 30)
1652
+ * - The 30x failure penalty means each failure costs 3x more than a confirmation gains.
1653
+ * This asymmetry reflects real-world trust: it takes many good actions to build trust
1654
+ * but only a few failures to lose it.
1655
+ * - Returns 50 (neutral) when no tasks exist, avoiding penalizing new agents.
1656
+ */
1657
+ computeTaskCompletionFactor(data) {
1658
+ const totalTasks = data.tasks.length;
1583
1659
  if (totalTasks === 0) {
1584
1660
  return {
1585
1661
  name: "task_completion",
1586
1662
  score: 50,
1587
1663
  // Neutral if no tasks
1588
- weight: 0.25,
1664
+ weight: WEIGHT_TASK_COMPLETION,
1589
1665
  description: "No tasks recorded yet"
1590
1666
  };
1591
1667
  }
1592
- const confirmed = tasks.filter((t) => t.status === "confirmed").length;
1593
- const failed = tasks.filter((t) => t.status === "failed").length;
1668
+ const confirmed = data.tasks.filter((t) => t.status === "confirmed").length;
1669
+ const failed = data.tasks.filter((t) => t.status === "failed").length;
1594
1670
  const completionRate = confirmed / totalTasks;
1595
1671
  const failureRate = failed / totalTasks;
1596
1672
  const score = Math.round(completionRate * 100 - failureRate * 30);
1597
1673
  return {
1598
1674
  name: "task_completion",
1599
1675
  score: clamp(score, 0, 100),
1600
- weight: 0.25,
1676
+ weight: WEIGHT_TASK_COMPLETION,
1601
1677
  description: `${confirmed}/${totalTasks} tasks confirmed (${Math.round(completionRate * 100)}% rate)`
1602
1678
  };
1603
1679
  }
1604
- computeAnomalyFrequencyFactor(agentId) {
1605
- const anomalies = this.store.queryAnomalies((a) => a.agentId === agentId);
1606
- const actions = this.store.getActionsByAgent(agentId);
1607
- const anomalyCount = anomalies.length;
1608
- const actionCount = actions.length;
1680
+ /**
1681
+ * Anomaly frequency factor: fewer anomalies relative to total actions = higher trust.
1682
+ *
1683
+ * Uses anomaly rate (anomalies / actions) with step-function scoring:
1684
+ * - 0% → 100 (clean record)
1685
+ * - <1% → 90 (near-perfect, occasional false positive acceptable)
1686
+ * - <5% → 70 (some noise but generally clean)
1687
+ * - <10% → 50 (neutral, warrants monitoring)
1688
+ * - <25% → 30 (concerning pattern)
1689
+ * - 25%+ → 10 (severe anomaly rate)
1690
+ *
1691
+ * Critical anomalies incur a -15 point penalty each, high anomalies -8 each.
1692
+ * This severity weighting ensures a single critical anomaly (e.g., sanctions hit)
1693
+ * has outsized impact compared to multiple low-severity anomalies.
1694
+ */
1695
+ computeAnomalyFrequencyFactor(data) {
1696
+ const anomalyCount = data.anomalies.length;
1697
+ const actionCount = data.actions.length;
1609
1698
  if (actionCount === 0) {
1610
1699
  return {
1611
1700
  name: "anomaly_frequency",
1612
1701
  score: 50,
1613
- weight: 0.25,
1702
+ weight: WEIGHT_ANOMALY,
1614
1703
  description: "No actions recorded yet"
1615
1704
  };
1616
1705
  }
@@ -1622,23 +1711,37 @@ var TrustScorer = class {
1622
1711
  else if (anomalyRate < 0.1) score = 50;
1623
1712
  else if (anomalyRate < 0.25) score = 30;
1624
1713
  else score = 10;
1625
- const criticalCount = anomalies.filter((a) => a.severity === "critical").length;
1626
- const highCount = anomalies.filter((a) => a.severity === "high").length;
1714
+ const criticalCount = data.anomalies.filter((a) => a.severity === "critical").length;
1715
+ const highCount = data.anomalies.filter((a) => a.severity === "high").length;
1627
1716
  const penaltyFromSeverity = criticalCount * 15 + highCount * 8;
1628
1717
  return {
1629
1718
  name: "anomaly_frequency",
1630
1719
  score: clamp(score - penaltyFromSeverity, 0, 100),
1631
- weight: 0.25,
1720
+ weight: WEIGHT_ANOMALY,
1632
1721
  description: `${anomalyCount} anomalies across ${actionCount} actions (${Math.round(anomalyRate * 100)}% rate)`
1633
1722
  };
1634
1723
  }
1635
- computeTransactionConsistencyFactor(agentId) {
1636
- const transactions = this.store.getTransactionsByAgent(agentId);
1724
+ /**
1725
+ * Transaction consistency factor: stable spending patterns indicate legitimate usage.
1726
+ *
1727
+ * Uses the coefficient of variation (CV = stdDev / mean) to measure amount regularity:
1728
+ * - CV < 0.1 → 95 (extremely consistent, e.g., recurring payroll)
1729
+ * - CV < 0.3 → 80 (fairly consistent with some variance)
1730
+ * - CV < 0.5 → 65 (moderate variance, common for operational spending)
1731
+ * - CV < 1.0 → 45 (high variance, warrants attention)
1732
+ * - CV < 2.0 → 30 (very erratic, potential structuring)
1733
+ * - CV 2.0+ → 15 (extreme variance, strong structuring indicator)
1734
+ *
1735
+ * Also penalizes -15 points when >80% of destinations are unique with >5 txs,
1736
+ * which is a common money-laundering pattern (spray-and-pray distribution).
1737
+ */
1738
+ computeTransactionConsistencyFactor(data) {
1739
+ const transactions = data.transactions;
1637
1740
  if (transactions.length < 2) {
1638
1741
  return {
1639
1742
  name: "transaction_consistency",
1640
1743
  score: 50,
1641
- weight: 0.2,
1744
+ weight: WEIGHT_TX_CONSISTENCY,
1642
1745
  description: "Insufficient transaction history for consistency analysis"
1643
1746
  };
1644
1747
  }
@@ -1647,7 +1750,7 @@ var TrustScorer = class {
1647
1750
  return {
1648
1751
  name: "transaction_consistency",
1649
1752
  score: 50,
1650
- weight: 0.2,
1753
+ weight: WEIGHT_TX_CONSISTENCY,
1651
1754
  description: "Insufficient valid amounts for consistency analysis"
1652
1755
  };
1653
1756
  }
@@ -1670,13 +1773,25 @@ var TrustScorer = class {
1670
1773
  return {
1671
1774
  name: "transaction_consistency",
1672
1775
  score: clamp(score, 0, 100),
1673
- weight: 0.2,
1776
+ weight: WEIGHT_TX_CONSISTENCY,
1674
1777
  description: `CV=${cv.toFixed(2)}, ${destinations.size} unique destinations across ${transactions.length} transactions`
1675
1778
  };
1676
1779
  }
1677
- computeComplianceAdherenceFactor(agentId) {
1678
- const tasks = this.store.queryTasks((t) => t.agentId === agentId);
1679
- const transactions = this.store.getTransactionsByAgent(agentId);
1780
+ /**
1781
+ * Compliance adherence factor: agents that follow the task-confirm-evidence workflow
1782
+ * demonstrate higher operational integrity.
1783
+ *
1784
+ * Scoring:
1785
+ * - Base score: 50 (neutral)
1786
+ * - +30 max for evidence rate (confirmedTasksWithEvidence / confirmedTasks)
1787
+ * - +20 max for coverage rate (tasks / transactions, capped at 1.0)
1788
+ *
1789
+ * The rationale: tasks with evidence prove the agent completed auditable work.
1790
+ * Coverage rate measures what fraction of financial activity has corresponding
1791
+ * task tracking — higher coverage means better compliance discipline.
1792
+ */
1793
+ computeComplianceAdherenceFactor(data) {
1794
+ const { tasks, transactions } = data;
1680
1795
  const confirmedTasks = tasks.filter((t) => t.status === "confirmed");
1681
1796
  const tasksWithEvidence = confirmedTasks.filter(
1682
1797
  (t) => t.providedEvidence !== null && Object.keys(t.providedEvidence).length > 0
@@ -1693,7 +1808,7 @@ var TrustScorer = class {
1693
1808
  return {
1694
1809
  name: "compliance_adherence",
1695
1810
  score: clamp(score, 0, 100),
1696
- weight: 0.15,
1811
+ weight: WEIGHT_COMPLIANCE,
1697
1812
  description: `${tasksWithEvidence.length} tasks with evidence, ${transactions.length} total transactions`
1698
1813
  };
1699
1814
  }
@@ -1709,6 +1824,20 @@ var TrustScorer = class {
1709
1824
  factors.push(this.computeRoundAmountRisk(tx));
1710
1825
  return factors;
1711
1826
  }
1827
+ /**
1828
+ * Amount risk: higher transaction amounts carry inherently higher risk.
1829
+ *
1830
+ * Tiers are aligned with common fintech thresholds:
1831
+ * - <$100: near-zero risk (5), micro-transactions
1832
+ * - <$1K: low risk (15), typical consumer spending
1833
+ * - <$10K: moderate (30), approaches CTR reporting thresholds
1834
+ * - <$50K: elevated (55), large business transactions
1835
+ * - <$100K: high (75), requires enhanced due diligence
1836
+ * - $100K+: very high (95), institutional-scale transfers
1837
+ *
1838
+ * Additional +20 penalty when amount exceeds 5x the agent's historical average,
1839
+ * detecting sudden spending spikes that could indicate account compromise.
1840
+ */
1712
1841
  computeAmountRisk(tx) {
1713
1842
  const amount = parseAmount(tx.amount);
1714
1843
  if (isNaN(amount)) {
@@ -1787,6 +1916,21 @@ var TrustScorer = class {
1787
1916
  description: `Agent anomaly rate: ${Math.round(anomalyRate * 100)}%`
1788
1917
  };
1789
1918
  }
1919
+ /**
1920
+ * Round amount risk: round numbers are a structuring indicator in AML heuristics.
1921
+ *
1922
+ * Money launderers often transact in round amounts (e.g., $10,000 exactly) or
1923
+ * just under regulatory thresholds (e.g., $9,500 to avoid $10K CTR filing).
1924
+ *
1925
+ * Scoring:
1926
+ * - Non-round amounts: 5 (baseline, most legitimate transactions)
1927
+ * - Multiples of $1,000: 15 (mildly suspicious)
1928
+ * - Multiples of $10,000: 25 (more suspicious)
1929
+ * - Amounts $9,000-$10,000: +20 penalty (classic structuring band)
1930
+ *
1931
+ * This factor alone rarely triggers flagging — it contributes to the composite
1932
+ * risk score alongside amount, frequency, and destination analysis.
1933
+ */
1790
1934
  computeRoundAmountRisk(tx) {
1791
1935
  const amount = parseAmount(tx.amount);
1792
1936
  if (isNaN(amount)) {
@@ -1809,15 +1953,15 @@ var TrustScorer = class {
1809
1953
  // Scoring helpers
1810
1954
  // --------------------------------------------------------------------------
1811
1955
  scoreToLevel(score) {
1812
- if (score >= 90) return "verified";
1813
- if (score >= 70) return "high";
1814
- if (score >= 50) return "medium";
1815
- if (score >= 30) return "low";
1956
+ if (score >= TRUST_LEVEL_VERIFIED) return "verified";
1957
+ if (score >= TRUST_LEVEL_HIGH) return "high";
1958
+ if (score >= TRUST_LEVEL_MEDIUM) return "medium";
1959
+ if (score >= TRUST_LEVEL_LOW) return "low";
1816
1960
  return "untrusted";
1817
1961
  }
1818
1962
  riskScoreToLevel(score) {
1819
- if (score >= 80) return "critical";
1820
- if (score >= 60) return "high";
1963
+ if (score >= RISK_BLOCK_THRESHOLD) return "critical";
1964
+ if (score >= RISK_FLAG_THRESHOLD) return "high";
1821
1965
  if (score >= 35) return "medium";
1822
1966
  return "low";
1823
1967
  }
@@ -2185,13 +2329,1106 @@ var AnomalyDetector = class {
2185
2329
  try {
2186
2330
  cb(anomaly);
2187
2331
  } catch (error) {
2188
- if (this.config.debug) {
2189
- console.debug("[Kontext] Anomaly callback error:", error);
2332
+ console.warn(
2333
+ `[Kontext] Anomaly callback error for ${anomaly.type} (${anomaly.id}):`,
2334
+ error instanceof Error ? error.message : error
2335
+ );
2336
+ }
2337
+ }
2338
+ }
2339
+ };
2340
+
2341
+ // src/integrations/ofac-sanctions.ts
2342
+ var SANCTIONED_ADDRESS_DATABASE = [
2343
+ // -------------------------------------------------------------------------
2344
+ // Lazarus Group / DPRK (North Korea) -- SDN Active
2345
+ // -------------------------------------------------------------------------
2346
+ {
2347
+ address: "0x098B716B8Aaf21512996dC57EB0615e2383E2f96",
2348
+ lists: ["SDN"],
2349
+ entityName: "Lazarus Group",
2350
+ entityType: "GROUP",
2351
+ dateAdded: "2022-04-14",
2352
+ dateRemoved: null,
2353
+ chains: ["ethereum"],
2354
+ notes: "Ronin Bridge hack - primary address"
2355
+ },
2356
+ {
2357
+ address: "0xa0e1c89Ef1a489c9C7dE96311eD5Ce5D32c20E4B",
2358
+ lists: ["SDN"],
2359
+ entityName: "Lazarus Group",
2360
+ entityType: "GROUP",
2361
+ dateAdded: "2022-04-14",
2362
+ dateRemoved: null,
2363
+ chains: ["ethereum"],
2364
+ notes: "Ronin Bridge hack - associated address"
2365
+ },
2366
+ {
2367
+ address: "0x3Cffd56B47B7b41c56258D9C7731ABaDc360E460",
2368
+ lists: ["SDN"],
2369
+ entityName: "Lazarus Group",
2370
+ entityType: "GROUP",
2371
+ dateAdded: "2022-04-14",
2372
+ dateRemoved: null,
2373
+ chains: ["ethereum"],
2374
+ notes: "Ronin Bridge hack - associated address"
2375
+ },
2376
+ {
2377
+ address: "0x53b6936513e738f44FB50d2b9476730C0Ab3Bfc1",
2378
+ lists: ["SDN"],
2379
+ entityName: "Lazarus Group",
2380
+ entityType: "GROUP",
2381
+ dateAdded: "2022-04-14",
2382
+ dateRemoved: null,
2383
+ chains: ["ethereum"],
2384
+ notes: "Ronin Bridge hack - associated address"
2385
+ },
2386
+ {
2387
+ address: "0x4F47Bc496083C727c5fbe3CE9CDf2B0f6496270c",
2388
+ lists: ["SDN"],
2389
+ entityName: "Lazarus Group",
2390
+ entityType: "GROUP",
2391
+ dateAdded: "2023-08-22",
2392
+ dateRemoved: null,
2393
+ chains: ["ethereum"],
2394
+ notes: "Harmony Horizon bridge hack - associated address"
2395
+ },
2396
+ {
2397
+ address: "0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f",
2398
+ lists: ["SDN"],
2399
+ entityName: "Lazarus Group",
2400
+ entityType: "GROUP",
2401
+ dateAdded: "2023-08-22",
2402
+ dateRemoved: null,
2403
+ chains: ["ethereum"],
2404
+ notes: "Additional Lazarus-attributed address"
2405
+ },
2406
+ // -------------------------------------------------------------------------
2407
+ // Roman Semenov (Tornado Cash developer) -- SDN Active
2408
+ // Remains personally sanctioned even after Tornado Cash delisting
2409
+ // -------------------------------------------------------------------------
2410
+ {
2411
+ address: "0xdcbEfFBECcE100cCE9E4b153C4e15cB885643193",
2412
+ lists: ["SDN"],
2413
+ entityName: "Roman Semenov",
2414
+ entityType: "INDIVIDUAL",
2415
+ dateAdded: "2022-08-08",
2416
+ dateRemoved: null,
2417
+ chains: ["ethereum"],
2418
+ notes: "Tornado Cash developer - personal wallet"
2419
+ },
2420
+ {
2421
+ address: "0x931546D9e66836AbF687d2bc64B30407bAc8C568",
2422
+ lists: ["SDN"],
2423
+ entityName: "Roman Semenov",
2424
+ entityType: "INDIVIDUAL",
2425
+ dateAdded: "2022-08-08",
2426
+ dateRemoved: null,
2427
+ chains: ["ethereum"],
2428
+ notes: "Tornado Cash developer - personal wallet"
2429
+ },
2430
+ {
2431
+ address: "0x43fa21d92141BA9db43052492E0DeEE5aa5f0A93",
2432
+ lists: ["SDN"],
2433
+ entityName: "Roman Semenov",
2434
+ entityType: "INDIVIDUAL",
2435
+ dateAdded: "2022-08-08",
2436
+ dateRemoved: null,
2437
+ chains: ["ethereum"],
2438
+ notes: "Tornado Cash developer - personal wallet"
2439
+ },
2440
+ // -------------------------------------------------------------------------
2441
+ // Garantex exchange -- SDN Active
2442
+ // -------------------------------------------------------------------------
2443
+ {
2444
+ address: "0x6F1cA141A28907F78Ebaa64f83E4AE6038d3cbe7",
2445
+ lists: ["SDN"],
2446
+ entityName: "Garantex",
2447
+ entityType: "EXCHANGE",
2448
+ dateAdded: "2022-04-05",
2449
+ dateRemoved: null,
2450
+ chains: ["ethereum"],
2451
+ notes: "Russia-based exchange sanctioned for facilitating ransomware payments"
2452
+ },
2453
+ {
2454
+ address: "0x2f389cE8bD8ff92De3402FFCe4691d17fC4f6535",
2455
+ lists: ["SDN"],
2456
+ entityName: "Garantex",
2457
+ entityType: "EXCHANGE",
2458
+ dateAdded: "2022-04-05",
2459
+ dateRemoved: null,
2460
+ chains: ["ethereum"],
2461
+ notes: "Garantex hot wallet"
2462
+ },
2463
+ {
2464
+ address: "0x19Aa5Fe80D33a56D56c78e82eA5E50E5d80b4Dff",
2465
+ lists: ["SDN"],
2466
+ entityName: "Garantex",
2467
+ entityType: "EXCHANGE",
2468
+ dateAdded: "2022-04-05",
2469
+ dateRemoved: null,
2470
+ chains: ["ethereum"],
2471
+ notes: "Garantex operational address"
2472
+ },
2473
+ // -------------------------------------------------------------------------
2474
+ // Blender.io -- SDN Active
2475
+ // -------------------------------------------------------------------------
2476
+ {
2477
+ address: "0x23773E65ed146A459791799d01336DB287f25334",
2478
+ lists: ["SDN"],
2479
+ entityName: "Blender.io",
2480
+ entityType: "MIXER",
2481
+ dateAdded: "2022-05-06",
2482
+ dateRemoved: null,
2483
+ chains: ["ethereum"],
2484
+ notes: "First mixer sanctioned by OFAC - used by Lazarus Group"
2485
+ },
2486
+ // -------------------------------------------------------------------------
2487
+ // Sinbad.io -- SDN Active
2488
+ // -------------------------------------------------------------------------
2489
+ {
2490
+ address: "0x722122dF12D4e14e13Ac3b6895a86e84145b6967",
2491
+ lists: ["SDN"],
2492
+ entityName: "Sinbad.io",
2493
+ entityType: "MIXER",
2494
+ dateAdded: "2023-11-29",
2495
+ dateRemoved: null,
2496
+ chains: ["ethereum"],
2497
+ notes: "Mixer sanctioned for North Korean money laundering. (Note: this address is commonly associated with Tornado Cash but was re-designated under Sinbad.io)"
2498
+ },
2499
+ // -------------------------------------------------------------------------
2500
+ // Tornado Cash smart contracts -- DELISTED (March 21, 2025)
2501
+ // Retained for risk scoring; interaction with formerly sanctioned
2502
+ // infrastructure is still a compliance risk indicator.
2503
+ // -------------------------------------------------------------------------
2504
+ {
2505
+ address: "0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b",
2506
+ lists: ["DELISTED"],
2507
+ entityName: "Tornado Cash",
2508
+ entityType: "PROTOCOL",
2509
+ dateAdded: "2022-08-08",
2510
+ dateRemoved: "2025-03-21",
2511
+ chains: ["ethereum"],
2512
+ notes: "Tornado Cash 1 ETH pool - DELISTED per Fifth Circuit ruling"
2513
+ },
2514
+ {
2515
+ address: "0xd96f2B1c14Db8458374d9Aca76E26c3D18364307",
2516
+ lists: ["DELISTED"],
2517
+ entityName: "Tornado Cash",
2518
+ entityType: "PROTOCOL",
2519
+ dateAdded: "2022-08-08",
2520
+ dateRemoved: "2025-03-21",
2521
+ chains: ["ethereum"],
2522
+ notes: "Tornado Cash 10 ETH pool - DELISTED"
2523
+ },
2524
+ {
2525
+ address: "0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBfA9",
2526
+ lists: ["DELISTED"],
2527
+ entityName: "Tornado Cash",
2528
+ entityType: "PROTOCOL",
2529
+ dateAdded: "2022-08-08",
2530
+ dateRemoved: "2025-03-21",
2531
+ chains: ["ethereum"],
2532
+ notes: "Tornado Cash pool contract - DELISTED"
2533
+ },
2534
+ {
2535
+ address: "0xDD4c48C0B24039969fC16D1cdF626eaB821d3384",
2536
+ lists: ["DELISTED"],
2537
+ entityName: "Tornado Cash",
2538
+ entityType: "PROTOCOL",
2539
+ dateAdded: "2022-08-08",
2540
+ dateRemoved: "2025-03-21",
2541
+ chains: ["ethereum"],
2542
+ notes: "Tornado Cash 100 ETH pool - DELISTED"
2543
+ },
2544
+ {
2545
+ address: "0xd4B88Df4D29F5CedD6857912842cff3b20C8Cfa3",
2546
+ lists: ["DELISTED"],
2547
+ entityName: "Tornado Cash",
2548
+ entityType: "PROTOCOL",
2549
+ dateAdded: "2022-08-08",
2550
+ dateRemoved: "2025-03-21",
2551
+ chains: ["ethereum"],
2552
+ notes: "Tornado Cash 100 DAI pool - DELISTED"
2553
+ },
2554
+ {
2555
+ address: "0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF",
2556
+ lists: ["DELISTED"],
2557
+ entityName: "Tornado Cash",
2558
+ entityType: "PROTOCOL",
2559
+ dateAdded: "2022-08-08",
2560
+ dateRemoved: "2025-03-21",
2561
+ chains: ["ethereum"],
2562
+ notes: "Tornado Cash 10000 DAI pool - DELISTED"
2563
+ },
2564
+ {
2565
+ address: "0xA160cdAB225685dA1d56aa342Ad8841c3b53f291",
2566
+ lists: ["DELISTED"],
2567
+ entityName: "Tornado Cash",
2568
+ entityType: "PROTOCOL",
2569
+ dateAdded: "2022-08-08",
2570
+ dateRemoved: "2025-03-21",
2571
+ chains: ["ethereum"],
2572
+ notes: "Tornado Cash 100000 DAI pool - DELISTED"
2573
+ },
2574
+ {
2575
+ address: "0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144",
2576
+ lists: ["DELISTED"],
2577
+ entityName: "Tornado Cash",
2578
+ entityType: "PROTOCOL",
2579
+ dateAdded: "2022-08-08",
2580
+ dateRemoved: "2025-03-21",
2581
+ chains: ["ethereum"],
2582
+ notes: "Tornado Cash 1000 USDC pool - DELISTED"
2583
+ },
2584
+ {
2585
+ address: "0xF60dD140cFf0706bAE9Cd734Ac3683731B816EeD",
2586
+ lists: ["DELISTED"],
2587
+ entityName: "Tornado Cash",
2588
+ entityType: "PROTOCOL",
2589
+ dateAdded: "2022-11-08",
2590
+ dateRemoved: "2025-03-21",
2591
+ chains: ["ethereum"],
2592
+ notes: "Tornado Cash - added November 2022 update - DELISTED"
2593
+ },
2594
+ {
2595
+ address: "0x22aaA7720ddd5388A3c0A3333430953C68f1849b",
2596
+ lists: ["DELISTED"],
2597
+ entityName: "Tornado Cash",
2598
+ entityType: "PROTOCOL",
2599
+ dateAdded: "2022-08-08",
2600
+ dateRemoved: "2025-03-21",
2601
+ chains: ["ethereum"],
2602
+ notes: "Tornado Cash - DELISTED"
2603
+ },
2604
+ {
2605
+ address: "0xBA214C1c1928a32Bffe790263E38B4Af9bFCD659",
2606
+ lists: ["DELISTED"],
2607
+ entityName: "Tornado Cash",
2608
+ entityType: "PROTOCOL",
2609
+ dateAdded: "2022-08-08",
2610
+ dateRemoved: "2025-03-21",
2611
+ chains: ["ethereum"],
2612
+ notes: "Tornado Cash - DELISTED"
2613
+ },
2614
+ {
2615
+ address: "0xb1C8094B234DcE6e03f10a5b673c1d8C69739A00",
2616
+ lists: ["DELISTED"],
2617
+ entityName: "Tornado Cash",
2618
+ entityType: "PROTOCOL",
2619
+ dateAdded: "2022-08-08",
2620
+ dateRemoved: "2025-03-21",
2621
+ chains: ["ethereum"],
2622
+ notes: "Tornado Cash - DELISTED"
2623
+ },
2624
+ {
2625
+ address: "0x527653eA119F3E6a1F5BD18fbF4714081D7B31ce",
2626
+ lists: ["DELISTED"],
2627
+ entityName: "Tornado Cash",
2628
+ entityType: "PROTOCOL",
2629
+ dateAdded: "2022-08-08",
2630
+ dateRemoved: "2025-03-21",
2631
+ chains: ["ethereum"],
2632
+ notes: "Tornado Cash - DELISTED"
2633
+ },
2634
+ {
2635
+ address: "0x58E8dCC13BE9780fC42E8723D8EaD4CF46943dF2",
2636
+ lists: ["DELISTED"],
2637
+ entityName: "Tornado Cash",
2638
+ entityType: "PROTOCOL",
2639
+ dateAdded: "2022-08-08",
2640
+ dateRemoved: "2025-03-21",
2641
+ chains: ["ethereum"],
2642
+ notes: "Tornado Cash Router - DELISTED"
2643
+ },
2644
+ {
2645
+ address: "0x8589427373D6D84E98730D7795D8f6f8731FDA16",
2646
+ lists: ["DELISTED"],
2647
+ entityName: "Tornado Cash",
2648
+ entityType: "PROTOCOL",
2649
+ dateAdded: "2022-08-08",
2650
+ dateRemoved: "2025-03-21",
2651
+ chains: ["ethereum"],
2652
+ notes: "Tornado Cash - DELISTED"
2653
+ },
2654
+ // -------------------------------------------------------------------------
2655
+ // Zedcex / Zedxion -- SDN Active (IRGC-linked)
2656
+ // First-ever designation of an IRGC-linked digital asset exchange (2024)
2657
+ // -------------------------------------------------------------------------
2658
+ {
2659
+ address: "0xaeAAc358560e11f52454D997AAFF2c5731B6f8a6",
2660
+ lists: ["SDN"],
2661
+ entityName: "Zedcex Exchange",
2662
+ entityType: "EXCHANGE",
2663
+ dateAdded: "2024-06-26",
2664
+ dateRemoved: null,
2665
+ chains: ["ethereum"],
2666
+ notes: "IRGC-linked digital asset exchange"
2667
+ },
2668
+ // -------------------------------------------------------------------------
2669
+ // Additional known sanctioned addresses from SDN list
2670
+ // -------------------------------------------------------------------------
2671
+ {
2672
+ address: "0x7F367cC41522cE07553e823bf3be79A889DEbe1B",
2673
+ lists: ["SDN"],
2674
+ entityName: "DPRK IT Workers",
2675
+ entityType: "GROUP",
2676
+ dateAdded: "2023-05-23",
2677
+ dateRemoved: null,
2678
+ chains: ["ethereum"],
2679
+ notes: "North Korean IT worker network generating revenue for WMD programs"
2680
+ },
2681
+ {
2682
+ address: "0x01e2919679362dFBC9ee1644Ba9C6da6D6245BB1",
2683
+ lists: ["SDN"],
2684
+ entityName: "DPRK Cyber Operations",
2685
+ entityType: "GROUP",
2686
+ dateAdded: "2023-04-24",
2687
+ dateRemoved: null,
2688
+ chains: ["ethereum"],
2689
+ notes: "DPRK-attributed cyber theft proceeds"
2690
+ },
2691
+ {
2692
+ address: "0xc455f7fd3e0e12afd51fba5c106909934d8a0e4a",
2693
+ lists: ["SDN"],
2694
+ entityName: "DPRK Cyber Operations",
2695
+ entityType: "GROUP",
2696
+ dateAdded: "2023-08-22",
2697
+ dateRemoved: null,
2698
+ chains: ["ethereum"],
2699
+ notes: "Stake.com hack proceeds - DPRK attributed"
2700
+ }
2701
+ ];
2702
+ var SANCTIONED_JURISDICTIONS = [
2703
+ {
2704
+ code: "KP",
2705
+ name: "North Korea (DPRK)",
2706
+ sanctionsProgram: "North Korea Sanctions Regulations, 31 C.F.R. Part 510",
2707
+ riskLevel: "BLOCKED",
2708
+ comprehensive: true
2709
+ },
2710
+ {
2711
+ code: "IR",
2712
+ name: "Iran",
2713
+ sanctionsProgram: "Iranian Transactions and Sanctions Regulations, 31 C.F.R. Part 560",
2714
+ riskLevel: "BLOCKED",
2715
+ comprehensive: true
2716
+ },
2717
+ {
2718
+ code: "CU",
2719
+ name: "Cuba",
2720
+ sanctionsProgram: "Cuban Assets Control Regulations, 31 C.F.R. Part 515",
2721
+ riskLevel: "BLOCKED",
2722
+ comprehensive: true
2723
+ },
2724
+ {
2725
+ code: "SY",
2726
+ name: "Syria",
2727
+ sanctionsProgram: "Syrian Sanctions Regulations, 31 C.F.R. Part 542",
2728
+ riskLevel: "BLOCKED",
2729
+ comprehensive: true
2730
+ },
2731
+ {
2732
+ code: "RU_CRIMEA",
2733
+ name: "Crimea Region of Ukraine (Russian-occupied)",
2734
+ sanctionsProgram: "Ukraine-/Russia-Related Sanctions, E.O. 13685",
2735
+ riskLevel: "BLOCKED",
2736
+ comprehensive: true
2737
+ },
2738
+ {
2739
+ code: "RU_DNR",
2740
+ name: "Donetsk People's Republic (Russian-occupied Ukraine)",
2741
+ sanctionsProgram: "Ukraine-/Russia-Related Sanctions, E.O. 14065",
2742
+ riskLevel: "BLOCKED",
2743
+ comprehensive: true
2744
+ },
2745
+ {
2746
+ code: "RU_LNR",
2747
+ name: "Luhansk People's Republic (Russian-occupied Ukraine)",
2748
+ sanctionsProgram: "Ukraine-/Russia-Related Sanctions, E.O. 14065",
2749
+ riskLevel: "BLOCKED",
2750
+ comprehensive: true
2751
+ },
2752
+ {
2753
+ code: "RU_ZAPORIZHZHIA",
2754
+ name: "Zaporizhzhia Region (Russian-occupied Ukraine)",
2755
+ sanctionsProgram: "Ukraine-/Russia-Related Sanctions, E.O. 14065",
2756
+ riskLevel: "BLOCKED",
2757
+ comprehensive: true
2758
+ },
2759
+ {
2760
+ code: "RU_KHERSON",
2761
+ name: "Kherson Region (Russian-occupied Ukraine)",
2762
+ sanctionsProgram: "Ukraine-/Russia-Related Sanctions, E.O. 14065",
2763
+ riskLevel: "BLOCKED",
2764
+ comprehensive: true
2765
+ },
2766
+ {
2767
+ code: "BY",
2768
+ name: "Belarus",
2769
+ sanctionsProgram: "Belarus Sanctions Regulations, 31 C.F.R. Part 548",
2770
+ riskLevel: "HIGH",
2771
+ comprehensive: false
2772
+ },
2773
+ {
2774
+ code: "VE",
2775
+ name: "Venezuela",
2776
+ sanctionsProgram: "Venezuela Sanctions Regulations, 31 C.F.R. Part 591",
2777
+ riskLevel: "HIGH",
2778
+ comprehensive: false
2779
+ },
2780
+ {
2781
+ code: "MM",
2782
+ name: "Myanmar (Burma)",
2783
+ sanctionsProgram: "Burmese Sanctions Regulations, 31 C.F.R. Part 525",
2784
+ riskLevel: "MEDIUM",
2785
+ comprehensive: false
2786
+ },
2787
+ {
2788
+ code: "SD",
2789
+ name: "Sudan",
2790
+ sanctionsProgram: "Sudanese Sanctions Regulations, 31 C.F.R. Part 538",
2791
+ riskLevel: "HIGH",
2792
+ comprehensive: false
2793
+ },
2794
+ {
2795
+ code: "SO",
2796
+ name: "Somalia",
2797
+ sanctionsProgram: "Somalia Sanctions Regulations, 31 C.F.R. Part 551",
2798
+ riskLevel: "MEDIUM",
2799
+ comprehensive: false
2800
+ }
2801
+ ];
2802
+ var SANCTIONED_ENTITIES = [
2803
+ {
2804
+ name: "Lazarus Group",
2805
+ aliases: ["HIDDEN COBRA", "Guardians of Peace", "APT38", "Bluenoroff", "Stardust Chollima", "TEMP.Hermit"],
2806
+ addresses: [
2807
+ "0x098B716B8Aaf21512996dC57EB0615e2383E2f96",
2808
+ "0xa0e1c89Ef1a489c9C7dE96311eD5Ce5D32c20E4B",
2809
+ "0x3Cffd56B47B7b41c56258D9C7731ABaDc360E460",
2810
+ "0x53b6936513e738f44FB50d2b9476730C0Ab3Bfc1",
2811
+ "0x4F47Bc496083C727c5fbe3CE9CDf2B0f6496270c"
2812
+ ],
2813
+ list: "SDN"
2814
+ },
2815
+ {
2816
+ name: "Roman Semenov",
2817
+ aliases: ["Roman Storm", "Tornado Cash Developer"],
2818
+ addresses: [
2819
+ "0xdcbEfFBECcE100cCE9E4b153C4e15cB885643193",
2820
+ "0x931546D9e66836AbF687d2bc64B30407bAc8C568",
2821
+ "0x43fa21d92141BA9db43052492E0DeEE5aa5f0A93"
2822
+ ],
2823
+ list: "SDN"
2824
+ },
2825
+ {
2826
+ name: "Garantex",
2827
+ aliases: ["Garantex Exchange", "garantex.io", "GARANTEX EUROPE OU"],
2828
+ addresses: [
2829
+ "0x6F1cA141A28907F78Ebaa64f83E4AE6038d3cbe7",
2830
+ "0x2f389cE8bD8ff92De3402FFCe4691d17fC4f6535",
2831
+ "0x19Aa5Fe80D33a56D56c78e82eA5E50E5d80b4Dff"
2832
+ ],
2833
+ list: "SDN"
2834
+ },
2835
+ {
2836
+ name: "Blender.io",
2837
+ aliases: ["Blender", "Blender Mixer"],
2838
+ addresses: ["0x23773E65ed146A459791799d01336DB287f25334"],
2839
+ list: "SDN"
2840
+ },
2841
+ {
2842
+ name: "Tornado Cash",
2843
+ aliases: ["TornadoCash", "TC", "Tornado"],
2844
+ addresses: [
2845
+ "0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b",
2846
+ "0xd96f2B1c14Db8458374d9Aca76E26c3D18364307",
2847
+ "0x58E8dCC13BE9780fC42E8723D8EaD4CF46943dF2"
2848
+ ],
2849
+ list: "DELISTED"
2850
+ },
2851
+ {
2852
+ name: "Zedcex Exchange",
2853
+ aliases: ["Zedxion", "Zedcex"],
2854
+ addresses: ["0xaeAAc358560e11f52454D997AAFF2c5731B6f8a6"],
2855
+ list: "SDN"
2856
+ }
2857
+ ];
2858
+ var activeAddressSet = /* @__PURE__ */ new Set();
2859
+ var allAddressSet = /* @__PURE__ */ new Set();
2860
+ var addressToEntry = /* @__PURE__ */ new Map();
2861
+ function rebuildIndexes() {
2862
+ activeAddressSet = /* @__PURE__ */ new Set();
2863
+ allAddressSet = /* @__PURE__ */ new Set();
2864
+ addressToEntry = /* @__PURE__ */ new Map();
2865
+ for (const entry of SANCTIONED_ADDRESS_DATABASE) {
2866
+ const lower = entry.address.toLowerCase();
2867
+ allAddressSet.add(lower);
2868
+ addressToEntry.set(lower, entry);
2869
+ const isActive = entry.dateRemoved === null;
2870
+ if (isActive) {
2871
+ activeAddressSet.add(lower);
2872
+ }
2873
+ }
2874
+ }
2875
+ rebuildIndexes();
2876
+ var listMetadata = {
2877
+ lastUpdated: "2025-03-21",
2878
+ addressCount: SANCTIONED_ADDRESS_DATABASE.length,
2879
+ entityCount: SANCTIONED_ENTITIES.length,
2880
+ sourceUrl: "https://www.treasury.gov/ofac/downloads/sdnlist.txt",
2881
+ version: "2025-03-21-initial"
2882
+ };
2883
+ var OFACSanctionsScreener = class {
2884
+ // --------------------------------------------------------------------------
2885
+ // Core Address Screening
2886
+ // --------------------------------------------------------------------------
2887
+ /**
2888
+ * Check if an address is on an active sanctions list.
2889
+ * Returns true only for currently sanctioned (non-delisted) addresses.
2890
+ */
2891
+ isActivelySanctioned(address) {
2892
+ return activeAddressSet.has(address.toLowerCase());
2893
+ }
2894
+ /**
2895
+ * Check if an address has ever appeared on any sanctions list,
2896
+ * including those that have been delisted.
2897
+ */
2898
+ hasAnySanctionsHistory(address) {
2899
+ return allAddressSet.has(address.toLowerCase());
2900
+ }
2901
+ /**
2902
+ * Get the full entry for a sanctioned address.
2903
+ */
2904
+ getAddressEntry(address) {
2905
+ return addressToEntry.get(address.toLowerCase());
2906
+ }
2907
+ /**
2908
+ * Perform a comprehensive sanctions screening on an address.
2909
+ * Checks against all lists, evaluates jurisdiction, and computes risk score.
2910
+ */
2911
+ screenAddress(address, context) {
2912
+ const lower = address.toLowerCase();
2913
+ const directMatches = [];
2914
+ const jurisdictionFlags = [];
2915
+ const patternFlags = [];
2916
+ const ownershipFlags = [];
2917
+ const recommendations = [];
2918
+ let riskScore = 0;
2919
+ const entry = addressToEntry.get(lower);
2920
+ if (entry) {
2921
+ const isActive = entry.dateRemoved === null;
2922
+ directMatches.push({
2923
+ matchedAddress: entry.address,
2924
+ list: entry.lists[0],
2925
+ entityName: entry.entityName,
2926
+ entityType: entry.entityType,
2927
+ confidence: 1,
2928
+ active: isActive
2929
+ });
2930
+ if (isActive) {
2931
+ riskScore = 100;
2932
+ recommendations.push(
2933
+ `BLOCK: Address ${address} is on the OFAC ${entry.lists.join(", ")} list as ${entry.entityName}. Transaction is prohibited under U.S. law.`
2934
+ );
2935
+ } else {
2936
+ riskScore = Math.max(riskScore, 45);
2937
+ recommendations.push(
2938
+ `CAUTION: Address ${address} was previously sanctioned as ${entry.entityName} (${entry.lists.join(", ")}). Delisted on ${entry.dateRemoved}. Enhanced due diligence recommended.`
2939
+ );
2940
+ }
2941
+ }
2942
+ if (context?.counterpartyAddress) {
2943
+ const cpLower = context.counterpartyAddress.toLowerCase();
2944
+ const cpEntry = addressToEntry.get(cpLower);
2945
+ if (cpEntry) {
2946
+ const cpActive = cpEntry.dateRemoved === null;
2947
+ directMatches.push({
2948
+ matchedAddress: cpEntry.address,
2949
+ list: cpEntry.lists[0],
2950
+ entityName: cpEntry.entityName,
2951
+ entityType: cpEntry.entityType,
2952
+ confidence: 1,
2953
+ active: cpActive
2954
+ });
2955
+ if (cpActive) {
2956
+ riskScore = 100;
2957
+ recommendations.push(
2958
+ `BLOCK: Counterparty address ${context.counterpartyAddress} is on the OFAC ${cpEntry.lists.join(", ")} list as ${cpEntry.entityName}.`
2959
+ );
2960
+ } else {
2961
+ riskScore = Math.max(riskScore, 45);
2962
+ recommendations.push(
2963
+ `CAUTION: Counterparty was previously sanctioned as ${cpEntry.entityName}.`
2964
+ );
2965
+ }
2966
+ }
2967
+ }
2968
+ if (context?.jurisdiction) {
2969
+ const jurisdictionInfo = SANCTIONED_JURISDICTIONS.find(
2970
+ (j) => j.code === context.jurisdiction
2971
+ );
2972
+ if (jurisdictionInfo) {
2973
+ jurisdictionFlags.push({
2974
+ jurisdiction: jurisdictionInfo.code,
2975
+ name: jurisdictionInfo.name,
2976
+ reason: `Transaction involves ${jurisdictionInfo.name}, sanctioned under ${jurisdictionInfo.sanctionsProgram}`,
2977
+ riskLevel: jurisdictionInfo.riskLevel
2978
+ });
2979
+ if (jurisdictionInfo.comprehensive) {
2980
+ riskScore = 100;
2981
+ recommendations.push(
2982
+ `BLOCK: Transaction involves comprehensively sanctioned jurisdiction: ${jurisdictionInfo.name}. All transactions are prohibited under ${jurisdictionInfo.sanctionsProgram}.`
2983
+ );
2984
+ } else {
2985
+ riskScore = Math.max(riskScore, 70);
2986
+ recommendations.push(
2987
+ `REVIEW: Transaction involves partially sanctioned jurisdiction: ${jurisdictionInfo.name}. Enhanced screening required per ${jurisdictionInfo.sanctionsProgram}.`
2988
+ );
2989
+ }
2990
+ }
2991
+ }
2992
+ let riskLevel;
2993
+ if (riskScore >= 100) {
2994
+ riskLevel = "BLOCKED";
2995
+ } else if (riskScore >= 70) {
2996
+ riskLevel = "SEVERE";
2997
+ } else if (riskScore >= 50) {
2998
+ riskLevel = "HIGH";
2999
+ } else if (riskScore >= 30) {
3000
+ riskLevel = "MEDIUM";
3001
+ } else if (riskScore > 0) {
3002
+ riskLevel = "LOW";
3003
+ } else {
3004
+ riskLevel = "NONE";
3005
+ }
3006
+ const sanctioned = directMatches.some((m) => m.active);
3007
+ if (recommendations.length === 0) {
3008
+ recommendations.push("Address passed all sanctions screening checks.");
3009
+ }
3010
+ return {
3011
+ sanctioned,
3012
+ riskLevel,
3013
+ riskScore: Math.min(riskScore, 100),
3014
+ address,
3015
+ directMatches,
3016
+ jurisdictionFlags,
3017
+ patternFlags,
3018
+ ownershipFlags,
3019
+ screenedAt: (/* @__PURE__ */ new Date()).toISOString(),
3020
+ listsChecked: ["SDN", "SSI", "CONSOLIDATED", "DELISTED"],
3021
+ recommendations
3022
+ };
3023
+ }
3024
+ // --------------------------------------------------------------------------
3025
+ // Jurisdictional Screening
3026
+ // --------------------------------------------------------------------------
3027
+ /**
3028
+ * Screen a jurisdiction for sanctions.
3029
+ */
3030
+ screenJurisdiction(code) {
3031
+ const info = SANCTIONED_JURISDICTIONS.find(
3032
+ (j) => j.code === code || j.name.toLowerCase().includes(code.toLowerCase())
3033
+ );
3034
+ if (!info) return null;
3035
+ return {
3036
+ jurisdiction: info.code,
3037
+ name: info.name,
3038
+ reason: `Sanctioned under ${info.sanctionsProgram}`,
3039
+ riskLevel: info.riskLevel
3040
+ };
3041
+ }
3042
+ /**
3043
+ * Get all sanctioned jurisdictions.
3044
+ */
3045
+ getSanctionedJurisdictions() {
3046
+ return [...SANCTIONED_JURISDICTIONS];
3047
+ }
3048
+ /**
3049
+ * Check if a jurisdiction has comprehensive sanctions (all transactions blocked).
3050
+ */
3051
+ isComprehensiveSanctions(code) {
3052
+ const info = SANCTIONED_JURISDICTIONS.find((j) => j.code === code);
3053
+ return info?.comprehensive ?? false;
3054
+ }
3055
+ // --------------------------------------------------------------------------
3056
+ // Fuzzy Entity Name Matching
3057
+ // --------------------------------------------------------------------------
3058
+ /**
3059
+ * Search for sanctioned entities by name with fuzzy matching.
3060
+ * Uses normalized Levenshtein distance and alias matching.
3061
+ *
3062
+ * @param query - Name to search for
3063
+ * @param threshold - Minimum similarity score (0-1, default 0.6)
3064
+ * @returns Matching entities sorted by relevance
3065
+ */
3066
+ searchEntityName(query, threshold = 0.6) {
3067
+ const queryLower = query.toLowerCase().trim();
3068
+ const results = [];
3069
+ for (const entity of SANCTIONED_ENTITIES) {
3070
+ const nameSim = computeSimilarity(queryLower, entity.name.toLowerCase());
3071
+ if (nameSim >= threshold) {
3072
+ results.push({ entity, similarity: nameSim, matchedOn: entity.name });
3073
+ continue;
3074
+ }
3075
+ let bestAliasSim = 0;
3076
+ let bestAlias = "";
3077
+ for (const alias of entity.aliases) {
3078
+ const aliasSim = computeSimilarity(queryLower, alias.toLowerCase());
3079
+ if (aliasSim > bestAliasSim) {
3080
+ bestAliasSim = aliasSim;
3081
+ bestAlias = alias;
3082
+ }
3083
+ }
3084
+ if (bestAliasSim >= threshold) {
3085
+ results.push({ entity, similarity: bestAliasSim, matchedOn: bestAlias });
3086
+ continue;
3087
+ }
3088
+ const allNames = [entity.name, ...entity.aliases];
3089
+ for (const name of allNames) {
3090
+ if (name.toLowerCase().includes(queryLower) || queryLower.includes(name.toLowerCase())) {
3091
+ results.push({ entity, similarity: 0.8, matchedOn: name });
3092
+ break;
3093
+ }
3094
+ }
3095
+ }
3096
+ results.sort((a, b) => b.similarity - a.similarity);
3097
+ return results;
3098
+ }
3099
+ // --------------------------------------------------------------------------
3100
+ // 50% Rule Screening
3101
+ // --------------------------------------------------------------------------
3102
+ /**
3103
+ * Check if an entity may be subject to the OFAC 50% Rule.
3104
+ *
3105
+ * Under the 50% Rule, entities owned 50% or more (individually or in
3106
+ * aggregate) by one or more sanctioned persons are themselves blocked,
3107
+ * even if not explicitly named on the SDN list.
3108
+ *
3109
+ * This method checks a provided ownership structure against known
3110
+ * sanctioned entities.
3111
+ *
3112
+ * @param ownershipEntries - Array of { ownerName, ownershipPercentage } objects
3113
+ * @returns Array of OwnershipFlag for any sanctioned owners
3114
+ */
3115
+ checkFiftyPercentRule(entityName, ownershipEntries) {
3116
+ const flags = [];
3117
+ let totalSanctionedOwnership = 0;
3118
+ for (const owner of ownershipEntries) {
3119
+ const matches = this.searchEntityName(owner.ownerName, 0.7);
3120
+ if (matches.length > 0) {
3121
+ totalSanctionedOwnership += owner.ownershipPercentage;
3122
+ flags.push({
3123
+ entityName,
3124
+ sanctionedParent: matches[0].entity.name,
3125
+ ownershipPercentage: owner.ownershipPercentage,
3126
+ source: `Fuzzy match on "${owner.ownerName}" -> "${matches[0].matchedOn}" (${Math.round(matches[0].similarity * 100)}% match)`
3127
+ });
3128
+ }
3129
+ }
3130
+ if (totalSanctionedOwnership >= 50 && flags.length > 0) {
3131
+ flags.push({
3132
+ entityName,
3133
+ sanctionedParent: `Aggregate sanctioned ownership: ${totalSanctionedOwnership}%`,
3134
+ ownershipPercentage: totalSanctionedOwnership,
3135
+ source: "OFAC 50% Rule: Entity is considered blocked due to aggregate sanctioned ownership >= 50%"
3136
+ });
3137
+ }
3138
+ return flags;
3139
+ }
3140
+ // --------------------------------------------------------------------------
3141
+ // Transaction Pattern Analysis
3142
+ // --------------------------------------------------------------------------
3143
+ /**
3144
+ * Analyze a set of transactions for patterns associated with sanctions evasion.
3145
+ *
3146
+ * Detects:
3147
+ * - Mixing/tumbling indicators (interaction with known mixers)
3148
+ * - Chain-hopping patterns (rapid cross-chain movements)
3149
+ * - Structuring (amounts just below reporting thresholds)
3150
+ * - Rapid fund movement through intermediaries
3151
+ * - Peeling chain patterns
3152
+ */
3153
+ analyzeTransactionPatterns(transactions) {
3154
+ const flags = [];
3155
+ if (transactions.length === 0) return flags;
3156
+ const mixerFlag = this.detectMixingPattern(transactions);
3157
+ if (mixerFlag) flags.push(mixerFlag);
3158
+ const chainHopFlag = this.detectChainHopping(transactions);
3159
+ if (chainHopFlag) flags.push(chainHopFlag);
3160
+ const structuringFlag = this.detectStructuring(transactions);
3161
+ if (structuringFlag) flags.push(structuringFlag);
3162
+ const rapidFlag = this.detectRapidMovement(transactions);
3163
+ if (rapidFlag) flags.push(rapidFlag);
3164
+ const peelingFlag = this.detectPeelingChain(transactions);
3165
+ if (peelingFlag) flags.push(peelingFlag);
3166
+ return flags;
3167
+ }
3168
+ detectMixingPattern(txs) {
3169
+ const evidence = [];
3170
+ for (const tx of txs) {
3171
+ const fromEntry = addressToEntry.get(tx.from.toLowerCase());
3172
+ const toEntry = addressToEntry.get(tx.to.toLowerCase());
3173
+ if (fromEntry?.entityType === "MIXER") {
3174
+ evidence.push(`Received funds from known mixer: ${fromEntry.entityName} (tx: ${tx.txHash})`);
3175
+ }
3176
+ if (toEntry?.entityType === "MIXER") {
3177
+ evidence.push(`Sent funds to known mixer: ${toEntry.entityName} (tx: ${tx.txHash})`);
3178
+ }
3179
+ }
3180
+ if (evidence.length > 0) {
3181
+ return {
3182
+ pattern: "MIXING",
3183
+ description: "Transaction history includes interaction with known mixing/tumbling services",
3184
+ severity: "HIGH",
3185
+ evidence
3186
+ };
3187
+ }
3188
+ return null;
3189
+ }
3190
+ detectChainHopping(txs) {
3191
+ const chainTimestamps = /* @__PURE__ */ new Map();
3192
+ for (const tx of txs) {
3193
+ const timestamps = chainTimestamps.get(tx.chain) ?? [];
3194
+ timestamps.push(new Date(tx.timestamp).getTime());
3195
+ chainTimestamps.set(tx.chain, timestamps);
3196
+ }
3197
+ if (chainTimestamps.size < 2) return null;
3198
+ const allTimestamps = [];
3199
+ for (const [chain, times] of chainTimestamps) {
3200
+ for (const time of times) {
3201
+ allTimestamps.push({ chain, time });
3202
+ }
3203
+ }
3204
+ allTimestamps.sort((a, b) => a.time - b.time);
3205
+ const evidence = [];
3206
+ const RAPID_WINDOW_MS = 10 * 60 * 1e3;
3207
+ for (let i = 1; i < allTimestamps.length; i++) {
3208
+ const prev = allTimestamps[i - 1];
3209
+ const curr = allTimestamps[i];
3210
+ if (prev && curr && prev.chain !== curr.chain) {
3211
+ const gap = curr.time - prev.time;
3212
+ if (gap < RAPID_WINDOW_MS) {
3213
+ evidence.push(
3214
+ `Cross-chain hop: ${prev.chain} -> ${curr.chain} within ${Math.round(gap / 1e3)}s`
3215
+ );
3216
+ }
3217
+ }
3218
+ }
3219
+ if (evidence.length >= 2) {
3220
+ return {
3221
+ pattern: "CHAIN_HOPPING",
3222
+ description: "Rapid cross-chain fund movements detected, a pattern associated with sanctions evasion",
3223
+ severity: "MEDIUM",
3224
+ evidence
3225
+ };
3226
+ }
3227
+ return null;
3228
+ }
3229
+ detectStructuring(txs) {
3230
+ const REPORTING_THRESHOLD2 = 1e4;
3231
+ const STRUCTURING_BAND_LOW = REPORTING_THRESHOLD2 * 0.8;
3232
+ const STRUCTURING_BAND_HIGH = REPORTING_THRESHOLD2;
3233
+ const structuredTxs = txs.filter(
3234
+ (tx) => tx.amount >= STRUCTURING_BAND_LOW && tx.amount < STRUCTURING_BAND_HIGH
3235
+ );
3236
+ if (structuredTxs.length >= 3) {
3237
+ const evidence = structuredTxs.map(
3238
+ (tx) => `$${tx.amount.toFixed(2)} on ${tx.chain} (tx: ${tx.txHash})`
3239
+ );
3240
+ return {
3241
+ pattern: "STRUCTURING",
3242
+ description: `${structuredTxs.length} transactions clustered just below the $${REPORTING_THRESHOLD2} reporting threshold, a pattern consistent with structuring`,
3243
+ severity: "HIGH",
3244
+ evidence
3245
+ };
3246
+ }
3247
+ return null;
3248
+ }
3249
+ detectRapidMovement(txs) {
3250
+ const sorted = [...txs].sort(
3251
+ (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
3252
+ );
3253
+ const RAPID_THRESHOLD_MS = 60 * 1e3;
3254
+ const evidence = [];
3255
+ for (let i = 1; i < sorted.length; i++) {
3256
+ const prev = sorted[i - 1];
3257
+ const curr = sorted[i];
3258
+ if (prev && curr) {
3259
+ const gap = new Date(curr.timestamp).getTime() - new Date(prev.timestamp).getTime();
3260
+ if (gap < RAPID_THRESHOLD_MS && gap >= 0) {
3261
+ evidence.push(
3262
+ `${curr.from} -> ${curr.to}: $${curr.amount} within ${Math.round(gap / 1e3)}s of previous tx`
3263
+ );
3264
+ }
3265
+ }
3266
+ }
3267
+ if (evidence.length >= 3) {
3268
+ return {
3269
+ pattern: "RAPID_MOVEMENT",
3270
+ description: "Rapid succession of fund movements detected, consistent with layering/obfuscation",
3271
+ severity: "MEDIUM",
3272
+ evidence
3273
+ };
3274
+ }
3275
+ return null;
3276
+ }
3277
+ detectPeelingChain(txs) {
3278
+ const sorted = [...txs].sort(
3279
+ (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
3280
+ );
3281
+ const evidence = [];
3282
+ let consecutiveDecreases = 0;
3283
+ for (let i = 1; i < sorted.length; i++) {
3284
+ const prev = sorted[i - 1];
3285
+ const curr = sorted[i];
3286
+ if (!prev || !curr) continue;
3287
+ if (curr.amount < prev.amount && curr.amount > 0) {
3288
+ const peeled = prev.amount - curr.amount;
3289
+ if (peeled < prev.amount * 0.3) {
3290
+ consecutiveDecreases++;
3291
+ evidence.push(
3292
+ `Peel: $${prev.amount.toFixed(2)} -> $${curr.amount.toFixed(2)} (peeled $${peeled.toFixed(2)})`
3293
+ );
2190
3294
  }
3295
+ } else {
3296
+ consecutiveDecreases = 0;
3297
+ }
3298
+ }
3299
+ if (consecutiveDecreases >= 3) {
3300
+ return {
3301
+ pattern: "PEELING_CHAIN",
3302
+ description: "Peeling chain pattern detected: sequential transactions with decreasing amounts to different addresses",
3303
+ severity: "MEDIUM",
3304
+ evidence
3305
+ };
3306
+ }
3307
+ return null;
3308
+ }
3309
+ // --------------------------------------------------------------------------
3310
+ // Sanctions List Management
3311
+ // --------------------------------------------------------------------------
3312
+ /**
3313
+ * Add new sanctioned addresses to the database at runtime.
3314
+ * Returns the count of newly added addresses.
3315
+ */
3316
+ addAddresses(entries) {
3317
+ let added = 0;
3318
+ for (const entry of entries) {
3319
+ const lower = entry.address.toLowerCase();
3320
+ if (!allAddressSet.has(lower)) {
3321
+ SANCTIONED_ADDRESS_DATABASE.push(entry);
3322
+ added++;
2191
3323
  }
2192
3324
  }
3325
+ if (added > 0) {
3326
+ rebuildIndexes();
3327
+ listMetadata = {
3328
+ ...listMetadata,
3329
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
3330
+ addressCount: SANCTIONED_ADDRESS_DATABASE.length,
3331
+ version: `runtime-update-${Date.now()}`
3332
+ };
3333
+ }
3334
+ return added;
3335
+ }
3336
+ /**
3337
+ * Add new entity names for fuzzy matching.
3338
+ */
3339
+ addEntities(entities) {
3340
+ let added = 0;
3341
+ for (const entity of entities) {
3342
+ const exists = SANCTIONED_ENTITIES.some(
3343
+ (e) => e.name.toLowerCase() === entity.name.toLowerCase()
3344
+ );
3345
+ if (!exists) {
3346
+ SANCTIONED_ENTITIES.push(entity);
3347
+ added++;
3348
+ }
3349
+ }
3350
+ if (added > 0) {
3351
+ listMetadata = {
3352
+ ...listMetadata,
3353
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
3354
+ entityCount: SANCTIONED_ENTITIES.length,
3355
+ version: `runtime-entity-update-${Date.now()}`
3356
+ };
3357
+ }
3358
+ return added;
3359
+ }
3360
+ /**
3361
+ * Get current sanctions list metadata.
3362
+ */
3363
+ getListMetadata() {
3364
+ return { ...listMetadata };
3365
+ }
3366
+ /**
3367
+ * Get all active sanctioned addresses (non-delisted).
3368
+ */
3369
+ getActiveSanctionedAddresses() {
3370
+ return [...activeAddressSet];
3371
+ }
3372
+ /**
3373
+ * Get all sanctioned addresses including delisted.
3374
+ */
3375
+ getAllAddresses() {
3376
+ return [...allAddressSet];
3377
+ }
3378
+ /**
3379
+ * Get the count of active sanctioned addresses.
3380
+ */
3381
+ getActiveAddressCount() {
3382
+ return activeAddressSet.size;
3383
+ }
3384
+ /**
3385
+ * Get the count of all addresses (including delisted).
3386
+ */
3387
+ getTotalAddressCount() {
3388
+ return allAddressSet.size;
3389
+ }
3390
+ /**
3391
+ * Get all entity names in the database.
3392
+ */
3393
+ getEntities() {
3394
+ return [...SANCTIONED_ENTITIES];
2193
3395
  }
2194
3396
  };
3397
+ function computeSimilarity(a, b) {
3398
+ if (a === b) return 1;
3399
+ if (a.length === 0 || b.length === 0) return 0;
3400
+ const maxLen = Math.max(a.length, b.length);
3401
+ const distance = levenshteinDistance(a, b);
3402
+ return 1 - distance / maxLen;
3403
+ }
3404
+ function levenshteinDistance(a, b) {
3405
+ const m = a.length;
3406
+ const n = b.length;
3407
+ const prev = new Array(n + 1);
3408
+ const curr = new Array(n + 1);
3409
+ for (let j = 0; j <= n; j++) {
3410
+ prev[j] = j;
3411
+ }
3412
+ for (let i = 1; i <= m; i++) {
3413
+ curr[0] = i;
3414
+ for (let j = 1; j <= n; j++) {
3415
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
3416
+ curr[j] = Math.min(
3417
+ (prev[j] ?? 0) + 1,
3418
+ // deletion
3419
+ (curr[j - 1] ?? 0) + 1,
3420
+ // insertion
3421
+ (prev[j - 1] ?? 0) + cost
3422
+ // substitution
3423
+ );
3424
+ }
3425
+ for (let j = 0; j <= n; j++) {
3426
+ prev[j] = curr[j] ?? 0;
3427
+ }
3428
+ }
3429
+ return prev[n] ?? 0;
3430
+ }
3431
+ var ofacScreener = new OFACSanctionsScreener();
2195
3432
 
2196
3433
  // src/integrations/usdc.ts
2197
3434
  var USDC_CONTRACTS = {
@@ -2204,7 +3441,33 @@ var USDC_CONTRACTS = {
2204
3441
  arc: "0xa0c0000000000000000000000000000000000001"
2205
3442
  };
2206
3443
  var SANCTIONED_ADDRESSES = [
2207
- // Tornado Cash contracts (sanctioned August 2022)
3444
+ // --- ACTIVELY SANCTIONED (SDN) ---
3445
+ // Lazarus Group / DPRK (Ronin Bridge hack)
3446
+ "0x098B716B8Aaf21512996dC57EB0615e2383E2f96",
3447
+ "0xa0e1c89Ef1a489c9C7dE96311eD5Ce5D32c20E4B",
3448
+ "0x3Cffd56B47B7b41c56258D9C7731ABaDc360E460",
3449
+ "0x53b6936513e738f44FB50d2b9476730C0Ab3Bfc1",
3450
+ // Lazarus Group - Harmony/Stake.com hacks
3451
+ "0x4F47Bc496083C727c5fbe3CE9CDf2B0f6496270c",
3452
+ "0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f",
3453
+ // DPRK Cyber Operations
3454
+ "0x7F367cC41522cE07553e823bf3be79A889DEbe1B",
3455
+ "0x01e2919679362dFBC9ee1644Ba9C6da6D6245BB1",
3456
+ "0xc455f7fd3e0e12afd51fba5c106909934d8a0e4a",
3457
+ // Garantex exchange (sanctioned April 2022)
3458
+ "0x6F1cA141A28907F78Ebaa64f83E4AE6038d3cbe7",
3459
+ "0x2f389cE8bD8ff92De3402FFCe4691d17fC4f6535",
3460
+ "0x19Aa5Fe80D33a56D56c78e82eA5E50E5d80b4Dff",
3461
+ // Blender.io (sanctioned May 2022)
3462
+ "0x23773E65ed146A459791799d01336DB287f25334",
3463
+ // Roman Semenov (Tornado Cash developer - REMAINS sanctioned)
3464
+ "0xdcbEfFBECcE100cCE9E4b153C4e15cB885643193",
3465
+ "0x931546D9e66836AbF687d2bc64B30407bAc8C568",
3466
+ "0x43fa21d92141BA9db43052492E0DeEE5aa5f0A93",
3467
+ // Zedcex / Zedxion (IRGC-linked, sanctioned June 2024)
3468
+ "0xaeAAc358560e11f52454D997AAFF2c5731B6f8a6",
3469
+ // --- DELISTED (formerly sanctioned, retained for backward compat) ---
3470
+ // Tornado Cash contracts (sanctioned Aug 2022, DELISTED March 21, 2025)
2208
3471
  "0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b",
2209
3472
  "0xd96f2B1c14Db8458374d9Aca76E26c3D18364307",
2210
3473
  "0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBfA9",
@@ -2222,17 +3485,8 @@ var SANCTIONED_ADDRESSES = [
2222
3485
  // Tornado Cash Router
2223
3486
  "0x8589427373D6D84E98730D7795D8f6f8731FDA16",
2224
3487
  // Tornado Cash
2225
- "0x722122dF12D4e14e13Ac3b6895a86e84145b6967",
2226
- // Tornado Cash
2227
- // Lazarus Group / North Korea (Ronin Bridge hack)
2228
- "0x098B716B8Aaf21512996dC57EB0615e2383E2f96",
2229
- "0xa0e1c89Ef1a489c9C7dE96311eD5Ce5D32c20E4B",
2230
- "0x3Cffd56B47B7b41c56258D9C7731ABaDc360E460",
2231
- "0x53b6936513e738f44FB50d2b9476730C0Ab3Bfc1",
2232
- // Garantex exchange (sanctioned April 2022)
2233
- "0x6F1cA141A28907F78Ebaa64f83E4AE6038d3cbe7",
2234
- // Blender.io
2235
- "0x23773E65ed146A459791799d01336DB287f25334"
3488
+ "0x722122dF12D4e14e13Ac3b6895a86e84145b6967"
3489
+ // Tornado Cash / Sinbad.io
2236
3490
  ];
2237
3491
  var SANCTIONED_SET = new Set(
2238
3492
  SANCTIONED_ADDRESSES.map((addr) => addr.toLowerCase())
@@ -2354,6 +3608,72 @@ var UsdcCompliance = class _UsdcCompliance {
2354
3608
  static getSupportedChains() {
2355
3609
  return Object.keys(USDC_CONTRACTS);
2356
3610
  }
3611
+ /**
3612
+ * Add new sanctioned addresses at runtime.
3613
+ * Normalizes all addresses to lowercase for consistent matching.
3614
+ * Skips addresses that are already in the list.
3615
+ *
3616
+ * @param addresses - Array of Ethereum addresses to add
3617
+ * @returns The count of newly added addresses (excluding duplicates)
3618
+ */
3619
+ static addSanctionedAddresses(addresses) {
3620
+ let added = 0;
3621
+ for (const addr of addresses) {
3622
+ const lower = addr.toLowerCase();
3623
+ if (!SANCTIONED_SET.has(lower)) {
3624
+ SANCTIONED_ADDRESSES.push(addr);
3625
+ SANCTIONED_SET.add(lower);
3626
+ added++;
3627
+ }
3628
+ }
3629
+ return added;
3630
+ }
3631
+ /**
3632
+ * Replace the entire sanctioned addresses list at runtime.
3633
+ * Clears existing entries and rebuilds from the provided array.
3634
+ *
3635
+ * @param addresses - The new complete list of sanctioned addresses
3636
+ */
3637
+ static replaceSanctionedAddresses(addresses) {
3638
+ SANCTIONED_ADDRESSES.length = 0;
3639
+ SANCTIONED_ADDRESSES.push(...addresses);
3640
+ SANCTIONED_SET.clear();
3641
+ for (const addr of addresses) {
3642
+ SANCTIONED_SET.add(addr.toLowerCase());
3643
+ }
3644
+ }
3645
+ /**
3646
+ * Get the current number of addresses in the sanctions list.
3647
+ *
3648
+ * @returns The size of the current sanctions list
3649
+ */
3650
+ static getSanctionsListSize() {
3651
+ return SANCTIONED_SET.size;
3652
+ }
3653
+ /**
3654
+ * Perform comprehensive OFAC sanctions screening using the advanced
3655
+ * OFACSanctionsScreener. This goes beyond simple address matching to
3656
+ * include jurisdictional screening, delisted address detection, and
3657
+ * entity metadata.
3658
+ *
3659
+ * @param address - The address to screen
3660
+ * @param context - Optional context for enhanced screening
3661
+ * @returns ComprehensiveSanctionsResult with full screening details
3662
+ */
3663
+ static screenComprehensive(address, context) {
3664
+ return ofacScreener.screenAddress(address, context);
3665
+ }
3666
+ /**
3667
+ * Check if an address is actively sanctioned (non-delisted).
3668
+ * Unlike isSanctioned(), this excludes addresses that have been removed
3669
+ * from the SDN list (e.g., Tornado Cash contracts post-March 2025).
3670
+ *
3671
+ * @param address - The address to check
3672
+ * @returns true if the address is on an active sanctions list
3673
+ */
3674
+ static isActivelySanctioned(address) {
3675
+ return ofacScreener.isActivelySanctioned(address);
3676
+ }
2357
3677
  // --------------------------------------------------------------------------
2358
3678
  // Individual compliance checks
2359
3679
  // --------------------------------------------------------------------------
@@ -2493,6 +3813,648 @@ var UsdcCompliance = class _UsdcCompliance {
2493
3813
  return recommendations;
2494
3814
  }
2495
3815
  };
3816
+
3817
+ // src/plans.ts
3818
+ var PLAN_LIMITS = {
3819
+ free: 2e4,
3820
+ pro: 1e5,
3821
+ // per user/seat
3822
+ enterprise: Infinity
3823
+ };
3824
+ var DEFAULT_WARNING_THRESHOLD = 0.8;
3825
+ var THROTTLE_INTERVAL = 100;
3826
+ var PlanManager = class _PlanManager {
3827
+ tier;
3828
+ seats;
3829
+ eventCount;
3830
+ billingPeriodStart;
3831
+ warningThreshold;
3832
+ warningEmitted = false;
3833
+ limitEmitted = false;
3834
+ eventsSinceLimitWarning = 0;
3835
+ usageWarningCallbacks = [];
3836
+ limitReachedCallbacks = [];
3837
+ /** Custom upgrade URL, configurable via init */
3838
+ upgradeUrl = "https://kontext.so/upgrade";
3839
+ /** Enterprise contact URL */
3840
+ enterpriseContactUrl = "https://cal.com/vinnaray";
3841
+ constructor(tier = "free", billingPeriodStart, seats = 1) {
3842
+ this.tier = tier;
3843
+ this.seats = Math.max(1, Math.floor(seats));
3844
+ this.eventCount = 0;
3845
+ this.warningThreshold = DEFAULT_WARNING_THRESHOLD;
3846
+ this.billingPeriodStart = billingPeriodStart ?? _PlanManager.defaultBillingPeriodStart();
3847
+ }
3848
+ /**
3849
+ * Returns the 1st of the current month at midnight UTC.
3850
+ */
3851
+ static defaultBillingPeriodStart() {
3852
+ const now2 = /* @__PURE__ */ new Date();
3853
+ return new Date(Date.UTC(now2.getUTCFullYear(), now2.getUTCMonth(), 1));
3854
+ }
3855
+ // --------------------------------------------------------------------------
3856
+ // Plan Info
3857
+ // --------------------------------------------------------------------------
3858
+ /** Get limits for a specific tier */
3859
+ static getPlanLimits(tier) {
3860
+ return { tier, eventLimit: PLAN_LIMITS[tier] };
3861
+ }
3862
+ /** Get the current plan tier */
3863
+ getTier() {
3864
+ return this.tier;
3865
+ }
3866
+ /** Get the event limit for the current plan (Pro is multiplied by seats) */
3867
+ getLimit() {
3868
+ const base = PLAN_LIMITS[this.tier];
3869
+ if (base === Infinity) return Infinity;
3870
+ if (this.tier === "pro") return base * this.seats;
3871
+ return base;
3872
+ }
3873
+ /** Get the current number of seats */
3874
+ getSeats() {
3875
+ return this.seats;
3876
+ }
3877
+ /** Update the number of seats (e.g., after Stripe subscription update) */
3878
+ setSeats(seats) {
3879
+ this.seats = Math.max(1, Math.floor(seats));
3880
+ this.warningEmitted = false;
3881
+ this.limitEmitted = false;
3882
+ this.eventsSinceLimitWarning = 0;
3883
+ }
3884
+ /** Get the current event count */
3885
+ getEventCount() {
3886
+ return this.eventCount;
3887
+ }
3888
+ /** Get the number of remaining events before the limit */
3889
+ getRemainingEvents() {
3890
+ const limit = this.getLimit();
3891
+ if (limit === Infinity) return Infinity;
3892
+ return Math.max(0, limit - this.eventCount);
3893
+ }
3894
+ /** Get the current usage as a percentage (0-100) */
3895
+ getUsagePercentage() {
3896
+ const limit = this.getLimit();
3897
+ if (limit === Infinity) return 0;
3898
+ return Math.min(100, this.eventCount / limit * 100);
3899
+ }
3900
+ /** Whether the event limit has been exceeded */
3901
+ isLimitExceeded() {
3902
+ const limit = this.getLimit();
3903
+ if (limit === Infinity) return false;
3904
+ return this.eventCount >= limit;
3905
+ }
3906
+ /** Get full usage object */
3907
+ getUsage() {
3908
+ return {
3909
+ plan: this.tier,
3910
+ seats: this.seats,
3911
+ eventCount: this.eventCount,
3912
+ limit: this.getLimit(),
3913
+ remainingEvents: this.getRemainingEvents(),
3914
+ usagePercentage: this.getUsagePercentage(),
3915
+ limitExceeded: this.isLimitExceeded()
3916
+ };
3917
+ }
3918
+ /** Get the billing period start date */
3919
+ getBillingPeriodStart() {
3920
+ return new Date(this.billingPeriodStart.getTime());
3921
+ }
3922
+ // --------------------------------------------------------------------------
3923
+ // Plan Management
3924
+ // --------------------------------------------------------------------------
3925
+ /**
3926
+ * Change the plan tier at runtime (e.g., after Stripe checkout succeeds).
3927
+ * This resets limit-related warning state since the new tier has different limits.
3928
+ */
3929
+ setPlan(tier) {
3930
+ this.tier = tier;
3931
+ this.warningEmitted = false;
3932
+ this.limitEmitted = false;
3933
+ this.eventsSinceLimitWarning = 0;
3934
+ }
3935
+ /**
3936
+ * Reset the event count for a new billing period.
3937
+ * Updates the billing period start to the given date or 1st of current month.
3938
+ */
3939
+ resetBillingPeriod(newStart) {
3940
+ this.eventCount = 0;
3941
+ this.billingPeriodStart = newStart ?? _PlanManager.defaultBillingPeriodStart();
3942
+ this.warningEmitted = false;
3943
+ this.limitEmitted = false;
3944
+ this.eventsSinceLimitWarning = 0;
3945
+ }
3946
+ /**
3947
+ * Check if the billing period should be reset (i.e., current date is past
3948
+ * the start of the next billing period). If so, auto-reset.
3949
+ */
3950
+ checkBillingPeriodReset() {
3951
+ const now2 = /* @__PURE__ */ new Date();
3952
+ const nextPeriodStart = new Date(
3953
+ Date.UTC(
3954
+ this.billingPeriodStart.getUTCFullYear(),
3955
+ this.billingPeriodStart.getUTCMonth() + 1,
3956
+ 1
3957
+ )
3958
+ );
3959
+ if (now2 >= nextPeriodStart) {
3960
+ this.resetBillingPeriod(
3961
+ new Date(Date.UTC(now2.getUTCFullYear(), now2.getUTCMonth(), 1))
3962
+ );
3963
+ return true;
3964
+ }
3965
+ return false;
3966
+ }
3967
+ // --------------------------------------------------------------------------
3968
+ // Event Tracking
3969
+ // --------------------------------------------------------------------------
3970
+ /**
3971
+ * Record an event and check thresholds.
3972
+ * Returns true if the event limit has been exceeded (soft limit).
3973
+ */
3974
+ recordEvent() {
3975
+ this.checkBillingPeriodReset();
3976
+ this.eventCount++;
3977
+ const limit = this.getLimit();
3978
+ if (limit === Infinity) return false;
3979
+ const usagePercentage = this.getUsagePercentage();
3980
+ if (!this.warningEmitted && usagePercentage >= this.warningThreshold * 100) {
3981
+ this.warningEmitted = true;
3982
+ const event = this.createLimitEvent("warning");
3983
+ for (const cb of this.usageWarningCallbacks) {
3984
+ cb(event);
3985
+ }
3986
+ }
3987
+ if (this.eventCount >= limit) {
3988
+ if (!this.limitEmitted) {
3989
+ this.limitEmitted = true;
3990
+ this.eventsSinceLimitWarning = 0;
3991
+ const event = this.createLimitEvent("limit_reached");
3992
+ for (const cb of this.limitReachedCallbacks) {
3993
+ cb(event);
3994
+ }
3995
+ this.logLimitMessage();
3996
+ } else {
3997
+ this.eventsSinceLimitWarning++;
3998
+ if (this.eventsSinceLimitWarning >= THROTTLE_INTERVAL) {
3999
+ this.eventsSinceLimitWarning = 0;
4000
+ const event = this.createLimitEvent("limit_reached");
4001
+ for (const cb of this.limitReachedCallbacks) {
4002
+ cb(event);
4003
+ }
4004
+ this.logLimitMessage();
4005
+ }
4006
+ }
4007
+ return true;
4008
+ }
4009
+ return false;
4010
+ }
4011
+ // --------------------------------------------------------------------------
4012
+ // Callbacks
4013
+ // --------------------------------------------------------------------------
4014
+ /**
4015
+ * Register a callback for the 80% usage warning.
4016
+ * @returns Unsubscribe function
4017
+ */
4018
+ onUsageWarning(callback) {
4019
+ this.usageWarningCallbacks.push(callback);
4020
+ return () => {
4021
+ const idx = this.usageWarningCallbacks.indexOf(callback);
4022
+ if (idx >= 0) this.usageWarningCallbacks.splice(idx, 1);
4023
+ };
4024
+ }
4025
+ /**
4026
+ * Register a callback for the limit reached event.
4027
+ * @returns Unsubscribe function
4028
+ */
4029
+ onLimitReached(callback) {
4030
+ this.limitReachedCallbacks.push(callback);
4031
+ return () => {
4032
+ const idx = this.limitReachedCallbacks.indexOf(callback);
4033
+ if (idx >= 0) this.limitReachedCallbacks.splice(idx, 1);
4034
+ };
4035
+ }
4036
+ // --------------------------------------------------------------------------
4037
+ // Persistence
4038
+ // --------------------------------------------------------------------------
4039
+ /** Serialize state for storage */
4040
+ toJSON() {
4041
+ return {
4042
+ tier: this.tier,
4043
+ seats: this.seats,
4044
+ eventCount: this.eventCount,
4045
+ billingPeriodStart: this.billingPeriodStart.toISOString()
4046
+ };
4047
+ }
4048
+ /** Restore state from storage */
4049
+ static fromJSON(data) {
4050
+ const manager = new _PlanManager(data.tier, new Date(data.billingPeriodStart), data.seats ?? 1);
4051
+ manager.eventCount = data.eventCount;
4052
+ return manager;
4053
+ }
4054
+ /** Set the event count directly (for restoring from storage) */
4055
+ setEventCount(count) {
4056
+ this.eventCount = count;
4057
+ }
4058
+ // --------------------------------------------------------------------------
4059
+ // Private
4060
+ // --------------------------------------------------------------------------
4061
+ createLimitEvent(type) {
4062
+ const message = type === "warning" ? `You've used ${this.getUsagePercentage().toFixed(0)}% of your ${this.tier} plan event limit (${this.eventCount}/${this.getLimit()}).` : `You've reached the ${this.getLimit().toLocaleString()} event limit on the ${this.tier === "free" ? "Free" : "Pro"} plan.`;
4063
+ return {
4064
+ type,
4065
+ plan: this.tier,
4066
+ eventCount: this.eventCount,
4067
+ limit: this.getLimit(),
4068
+ usagePercentage: this.getUsagePercentage(),
4069
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4070
+ message
4071
+ };
4072
+ }
4073
+ logLimitMessage() {
4074
+ if (this.tier === "free") {
4075
+ console.warn(
4076
+ `You've reached the 20,000 event limit on the Free plan. Upgrade to Pro for 100K events/user/mo and full compliance features \u2192 ${this.upgradeUrl}`
4077
+ );
4078
+ } else if (this.tier === "pro") {
4079
+ const effectiveLimit = (1e5 * this.seats).toLocaleString();
4080
+ console.warn(
4081
+ `You've reached the ${effectiveLimit} event limit on Pro (${this.seats} seat${this.seats !== 1 ? "s" : ""}). Add seats or contact us for Enterprise pricing \u2192 ${this.enterpriseContactUrl}`
4082
+ );
4083
+ }
4084
+ }
4085
+ };
4086
+ var NoopExporter = class {
4087
+ async export(_events) {
4088
+ return { success: true, exportedCount: 0 };
4089
+ }
4090
+ async flush() {
4091
+ }
4092
+ async shutdown() {
4093
+ }
4094
+ };
4095
+ var ConsoleExporter = class {
4096
+ prefix;
4097
+ constructor(options) {
4098
+ this.prefix = options?.prefix ?? "[Kontext Export]";
4099
+ }
4100
+ async export(events) {
4101
+ for (const event of events) {
4102
+ console.log(`${this.prefix} ${JSON.stringify(event)}`);
4103
+ }
4104
+ return { success: true, exportedCount: events.length };
4105
+ }
4106
+ async flush() {
4107
+ }
4108
+ async shutdown() {
4109
+ }
4110
+ };
4111
+ var JsonFileExporter = class {
4112
+ outputDir;
4113
+ buffer = [];
4114
+ bufferSize;
4115
+ constructor(options) {
4116
+ this.outputDir = path3__namespace.resolve(options?.outputDir ?? ".kontext/exports");
4117
+ this.bufferSize = options?.bufferSize ?? 1;
4118
+ }
4119
+ async export(events) {
4120
+ this.buffer.push(...events);
4121
+ if (this.buffer.length >= this.bufferSize) {
4122
+ await this.flush();
4123
+ }
4124
+ return { success: true, exportedCount: events.length };
4125
+ }
4126
+ async flush() {
4127
+ if (this.buffer.length === 0) return;
4128
+ const toWrite = [...this.buffer];
4129
+ this.buffer = [];
4130
+ try {
4131
+ fs3__namespace.mkdirSync(this.outputDir, { recursive: true });
4132
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
4133
+ const filePath = path3__namespace.join(this.outputDir, `events-${date}.jsonl`);
4134
+ const lines = toWrite.map((e) => JSON.stringify(e)).join("\n") + "\n";
4135
+ fs3__namespace.appendFileSync(filePath, lines, "utf-8");
4136
+ } catch (error) {
4137
+ console.warn("[Kontext JsonFileExporter] Failed to write events:", error);
4138
+ }
4139
+ }
4140
+ async shutdown() {
4141
+ await this.flush();
4142
+ }
4143
+ /** Get the output directory path. */
4144
+ getOutputDir() {
4145
+ return this.outputDir;
4146
+ }
4147
+ };
4148
+ var HttpExporter = class {
4149
+ endpoint;
4150
+ headers;
4151
+ batchSize;
4152
+ timeoutMs;
4153
+ buffer = [];
4154
+ constructor(options) {
4155
+ this.endpoint = options.endpoint;
4156
+ this.headers = {
4157
+ "Content-Type": "application/json",
4158
+ ...options.headers
4159
+ };
4160
+ this.batchSize = options.batchSize ?? 50;
4161
+ this.timeoutMs = options.timeoutMs ?? 3e4;
4162
+ }
4163
+ async export(events) {
4164
+ this.buffer.push(...events);
4165
+ if (this.buffer.length >= this.batchSize) {
4166
+ await this.flush();
4167
+ }
4168
+ return { success: true, exportedCount: events.length };
4169
+ }
4170
+ async flush() {
4171
+ if (this.buffer.length === 0) return;
4172
+ const toSend = [...this.buffer];
4173
+ this.buffer = [];
4174
+ try {
4175
+ const response = await fetch(this.endpoint, {
4176
+ method: "POST",
4177
+ headers: this.headers,
4178
+ body: JSON.stringify({ events: toSend }),
4179
+ signal: AbortSignal.timeout(this.timeoutMs)
4180
+ });
4181
+ if (!response.ok) {
4182
+ console.warn(
4183
+ `[Kontext HttpExporter] Export failed with status ${response.status}`
4184
+ );
4185
+ }
4186
+ } catch (error) {
4187
+ console.warn("[Kontext HttpExporter] Export failed:", error);
4188
+ }
4189
+ }
4190
+ async shutdown() {
4191
+ await this.flush();
4192
+ }
4193
+ };
4194
+ var KontextCloudExporter = class {
4195
+ apiKey;
4196
+ projectId;
4197
+ apiUrl;
4198
+ batchSize;
4199
+ timeoutMs;
4200
+ retryAttempts;
4201
+ retryDelayMs;
4202
+ buffer = [];
4203
+ constructor(options) {
4204
+ if (!options.apiKey) {
4205
+ throw new Error("KontextCloudExporter requires an API key (Pro or Enterprise plan)");
4206
+ }
4207
+ this.apiKey = options.apiKey;
4208
+ this.projectId = options.projectId;
4209
+ this.apiUrl = options.apiUrl ?? "https://api.getkontext.com";
4210
+ this.batchSize = options.batchSize ?? 100;
4211
+ this.timeoutMs = options.timeoutMs ?? 3e4;
4212
+ this.retryAttempts = options.retryAttempts ?? 3;
4213
+ this.retryDelayMs = options.retryDelayMs ?? 1e3;
4214
+ }
4215
+ async export(events) {
4216
+ this.buffer.push(...events);
4217
+ if (this.buffer.length >= this.batchSize) {
4218
+ await this.flush();
4219
+ }
4220
+ return { success: true, exportedCount: events.length };
4221
+ }
4222
+ async flush() {
4223
+ if (this.buffer.length === 0) return;
4224
+ const toSend = [...this.buffer];
4225
+ this.buffer = [];
4226
+ let lastError;
4227
+ for (let attempt = 0; attempt < this.retryAttempts; attempt++) {
4228
+ try {
4229
+ const response = await fetch(`${this.apiUrl}/v1/ingest`, {
4230
+ method: "POST",
4231
+ headers: {
4232
+ "Content-Type": "application/json",
4233
+ Authorization: `Bearer ${this.apiKey}`,
4234
+ "X-Project-Id": this.projectId,
4235
+ "X-SDK-Version": "0.1.0"
4236
+ },
4237
+ body: JSON.stringify({
4238
+ events: toSend,
4239
+ metadata: {
4240
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
4241
+ batchSize: toSend.length
4242
+ }
4243
+ }),
4244
+ signal: AbortSignal.timeout(this.timeoutMs)
4245
+ });
4246
+ if (response.ok) {
4247
+ return;
4248
+ }
4249
+ if (response.status >= 400 && response.status < 500) {
4250
+ console.warn(
4251
+ `[Kontext Cloud] Export rejected (${response.status}): ${await response.text()}`
4252
+ );
4253
+ return;
4254
+ }
4255
+ lastError = new Error(`HTTP ${response.status}`);
4256
+ } catch (error) {
4257
+ lastError = error;
4258
+ }
4259
+ if (attempt < this.retryAttempts - 1) {
4260
+ await new Promise(
4261
+ (resolve3) => setTimeout(resolve3, this.retryDelayMs * Math.pow(2, attempt))
4262
+ );
4263
+ }
4264
+ }
4265
+ console.warn(
4266
+ `[Kontext Cloud] Export failed after ${this.retryAttempts} attempts:`,
4267
+ lastError
4268
+ );
4269
+ }
4270
+ async shutdown() {
4271
+ await this.flush();
4272
+ }
4273
+ };
4274
+ var MultiExporter = class {
4275
+ exporters;
4276
+ constructor(exporters) {
4277
+ this.exporters = exporters;
4278
+ }
4279
+ async export(events) {
4280
+ const results = await Promise.allSettled(
4281
+ this.exporters.map((e) => e.export(events))
4282
+ );
4283
+ const failures = results.filter((r) => r.status === "rejected");
4284
+ if (failures.length > 0) {
4285
+ return {
4286
+ success: false,
4287
+ exportedCount: events.length,
4288
+ error: `${failures.length}/${this.exporters.length} exporters failed`
4289
+ };
4290
+ }
4291
+ return { success: true, exportedCount: events.length };
4292
+ }
4293
+ async flush() {
4294
+ await Promise.allSettled(this.exporters.map((e) => e.flush()));
4295
+ }
4296
+ async shutdown() {
4297
+ await Promise.allSettled(this.exporters.map((e) => e.shutdown()));
4298
+ }
4299
+ };
4300
+
4301
+ // src/feature-flags.ts
4302
+ var DEFAULT_CACHE_TTL_MS = 3e5;
4303
+ var FeatureFlagManager = class {
4304
+ config;
4305
+ cacheTtlMs;
4306
+ defaultValue;
4307
+ cache = /* @__PURE__ */ new Map();
4308
+ refreshInFlight = false;
4309
+ constructor(config) {
4310
+ this.config = config;
4311
+ this.cacheTtlMs = config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
4312
+ this.defaultValue = config.defaultValue ?? false;
4313
+ }
4314
+ // --------------------------------------------------------------------------
4315
+ // Public API
4316
+ // --------------------------------------------------------------------------
4317
+ /**
4318
+ * Initialize the manager by fetching all flags from Firestore.
4319
+ * Call this once at startup to warm the cache.
4320
+ */
4321
+ async init() {
4322
+ await this.refresh();
4323
+ }
4324
+ /**
4325
+ * Check if a flag is enabled for the given environment and plan.
4326
+ * Always synchronous — reads from cache.
4327
+ * Triggers a background refresh when the cached entry is stale.
4328
+ */
4329
+ isEnabled(flagName, environment, plan) {
4330
+ const env = environment ?? this.config.environment;
4331
+ const tier = plan ?? this.config.plan;
4332
+ const entry = this.cache.get(flagName);
4333
+ if (!entry) {
4334
+ this.triggerBackgroundRefresh();
4335
+ return this.defaultValue;
4336
+ }
4337
+ if (Date.now() > entry.expiresAt) {
4338
+ this.triggerBackgroundRefresh();
4339
+ }
4340
+ const envTargeting = entry.flag.targeting[env];
4341
+ if (!envTargeting) return this.defaultValue;
4342
+ return envTargeting[tier] ?? this.defaultValue;
4343
+ }
4344
+ /**
4345
+ * Get a single flag by name (from cache).
4346
+ */
4347
+ getFlag(flagName) {
4348
+ return this.cache.get(flagName)?.flag;
4349
+ }
4350
+ /**
4351
+ * Get all cached flags.
4352
+ */
4353
+ getAllFlags() {
4354
+ return Array.from(this.cache.values()).map((e) => e.flag);
4355
+ }
4356
+ /**
4357
+ * Force-refresh all flags from Firestore.
4358
+ */
4359
+ async refresh() {
4360
+ try {
4361
+ const docs = await this.fetchAllFromFirestore();
4362
+ const now2 = Date.now();
4363
+ for (const doc of docs) {
4364
+ const flag = parseFirestoreDocument(doc);
4365
+ if (!flag) continue;
4366
+ if (this.config.scope && flag.scope !== "all" && flag.scope !== this.config.scope) {
4367
+ continue;
4368
+ }
4369
+ this.cache.set(flag.name, {
4370
+ flag,
4371
+ expiresAt: now2 + this.cacheTtlMs
4372
+ });
4373
+ }
4374
+ } catch {
4375
+ }
4376
+ }
4377
+ // --------------------------------------------------------------------------
4378
+ // Firestore REST API
4379
+ // --------------------------------------------------------------------------
4380
+ get firestoreBaseUrl() {
4381
+ return `https://firestore.googleapis.com/v1/projects/${this.config.gcpProjectId}/databases/(default)/documents`;
4382
+ }
4383
+ async fetchAllFromFirestore() {
4384
+ const url = `${this.firestoreBaseUrl}/feature-flags`;
4385
+ const headers = {};
4386
+ if (this.config.accessToken) {
4387
+ headers["Authorization"] = `Bearer ${this.config.accessToken}`;
4388
+ }
4389
+ const res = await fetch(url, { headers });
4390
+ if (!res.ok) {
4391
+ throw new Error(`Firestore fetch failed: ${res.status} ${res.statusText}`);
4392
+ }
4393
+ const body = await res.json();
4394
+ return body.documents ?? [];
4395
+ }
4396
+ async fetchOneFromFirestore(flagName) {
4397
+ const url = `${this.firestoreBaseUrl}/feature-flags/${flagName}`;
4398
+ const headers = {};
4399
+ if (this.config.accessToken) {
4400
+ headers["Authorization"] = `Bearer ${this.config.accessToken}`;
4401
+ }
4402
+ const res = await fetch(url, { headers });
4403
+ if (res.status === 404) return null;
4404
+ if (!res.ok) {
4405
+ throw new Error(`Firestore fetch failed: ${res.status} ${res.statusText}`);
4406
+ }
4407
+ return await res.json();
4408
+ }
4409
+ // --------------------------------------------------------------------------
4410
+ // Background Refresh
4411
+ // --------------------------------------------------------------------------
4412
+ triggerBackgroundRefresh() {
4413
+ if (this.refreshInFlight) return;
4414
+ this.refreshInFlight = true;
4415
+ this.refresh().finally(() => {
4416
+ this.refreshInFlight = false;
4417
+ });
4418
+ }
4419
+ };
4420
+ function parseFirestoreDocument(doc) {
4421
+ try {
4422
+ const fields = doc.fields;
4423
+ const name = extractDocumentId(doc.name);
4424
+ const description = fields["description"]?.stringValue ?? "";
4425
+ const scope = fields["scope"]?.stringValue ?? "all";
4426
+ const createdBy = fields["createdBy"]?.stringValue ?? "";
4427
+ const createdAt = fields["createdAt"]?.stringValue ?? doc.createTime;
4428
+ const updatedAt = fields["updatedAt"]?.stringValue ?? doc.updateTime;
4429
+ const targetingMap = fields["targeting"]?.mapValue?.fields;
4430
+ if (!targetingMap) return null;
4431
+ const targeting = parseTargeting(targetingMap);
4432
+ if (!targeting) return null;
4433
+ return { name, description, scope, targeting, createdAt, updatedAt, createdBy };
4434
+ } catch {
4435
+ return null;
4436
+ }
4437
+ }
4438
+ function parseTargeting(fields) {
4439
+ const dev = parsePlanTargeting(fields["development"]?.mapValue?.fields);
4440
+ const staging = parsePlanTargeting(fields["staging"]?.mapValue?.fields);
4441
+ const prod = parsePlanTargeting(fields["production"]?.mapValue?.fields);
4442
+ if (!dev || !staging || !prod) return null;
4443
+ return { development: dev, staging, production: prod };
4444
+ }
4445
+ function parsePlanTargeting(fields) {
4446
+ if (!fields) return null;
4447
+ return {
4448
+ free: fields["free"]?.booleanValue ?? false,
4449
+ pro: fields["pro"]?.booleanValue ?? false,
4450
+ enterprise: fields["enterprise"]?.booleanValue ?? false
4451
+ };
4452
+ }
4453
+ function extractDocumentId(resourceName) {
4454
+ const parts = resourceName.split("/");
4455
+ return parts[parts.length - 1] ?? resourceName;
4456
+ }
4457
+ var PLAN_STORAGE_KEY = "kontext:plan";
2496
4458
  var Kontext = class _Kontext {
2497
4459
  config;
2498
4460
  store;
@@ -2502,18 +4464,34 @@ var Kontext = class _Kontext {
2502
4464
  trustScorer;
2503
4465
  anomalyDetector;
2504
4466
  mode;
4467
+ planManager;
4468
+ exporter;
4469
+ featureFlagManager;
2505
4470
  constructor(config) {
2506
4471
  this.config = config;
2507
4472
  this.mode = config.apiKey ? "cloud" : "local";
2508
4473
  this.store = new KontextStore();
4474
+ if (config.metadataSchema && typeof config.metadataSchema.parse !== "function") {
4475
+ throw new KontextError(
4476
+ "INITIALIZATION_ERROR" /* INITIALIZATION_ERROR */,
4477
+ "metadataSchema must have a parse() method"
4478
+ );
4479
+ }
2509
4480
  if (config.storage) {
2510
4481
  this.store.setStorageAdapter(config.storage);
2511
4482
  }
4483
+ const planTier = config.plan ?? "free";
4484
+ this.planManager = new PlanManager(planTier, void 0, config.seats ?? 1);
4485
+ if (config.upgradeUrl) {
4486
+ this.planManager.upgradeUrl = config.upgradeUrl;
4487
+ }
4488
+ this.exporter = config.exporter ?? new NoopExporter();
2512
4489
  this.logger = new ActionLogger(config, this.store);
2513
4490
  this.taskManager = new TaskManager(config, this.store);
2514
4491
  this.auditExporter = new AuditExporter(config, this.store);
2515
4492
  this.trustScorer = new TrustScorer(config, this.store);
2516
4493
  this.anomalyDetector = new AnomalyDetector(config, this.store);
4494
+ this.featureFlagManager = config.featureFlags ? new FeatureFlagManager(config.featureFlags) : null;
2517
4495
  }
2518
4496
  /**
2519
4497
  * Initialize the Kontext SDK.
@@ -2585,6 +4563,24 @@ var Kontext = class _Kontext {
2585
4563
  };
2586
4564
  }
2587
4565
  // --------------------------------------------------------------------------
4566
+ // Internal Helpers
4567
+ // --------------------------------------------------------------------------
4568
+ /**
4569
+ * Validate metadata against the configured schema, if any.
4570
+ * No-op when metadataSchema is not configured.
4571
+ */
4572
+ validateMetadata(metadata) {
4573
+ if (!metadata || !this.config.metadataSchema) return;
4574
+ try {
4575
+ this.config.metadataSchema.parse(metadata);
4576
+ } catch (err) {
4577
+ throw new KontextError(
4578
+ "VALIDATION_ERROR" /* VALIDATION_ERROR */,
4579
+ `Metadata validation failed: ${err instanceof Error ? err.message : String(err)}`
4580
+ );
4581
+ }
4582
+ }
4583
+ // --------------------------------------------------------------------------
2588
4584
  // Action Logging
2589
4585
  // --------------------------------------------------------------------------
2590
4586
  /**
@@ -2594,10 +4590,17 @@ var Kontext = class _Kontext {
2594
4590
  * @returns The created action log entry
2595
4591
  */
2596
4592
  async log(input) {
4593
+ this.validateMetadata(input.metadata);
2597
4594
  const action = await this.logger.log(input);
4595
+ const limitExceeded = this.planManager.recordEvent();
4596
+ if (limitExceeded) {
4597
+ action.metadata = { ...action.metadata, limitExceeded: true };
4598
+ }
2598
4599
  if (this.anomalyDetector.isEnabled()) {
2599
4600
  this.anomalyDetector.evaluateAction(action);
2600
4601
  }
4602
+ this.exporter.export([action]).catch(() => {
4603
+ });
2601
4604
  return action;
2602
4605
  }
2603
4606
  /**
@@ -2607,10 +4610,17 @@ var Kontext = class _Kontext {
2607
4610
  * @returns The created transaction record
2608
4611
  */
2609
4612
  async logTransaction(input) {
4613
+ this.validateMetadata(input.metadata);
2610
4614
  const record = await this.logger.logTransaction(input);
4615
+ const limitExceeded = this.planManager.recordEvent();
4616
+ if (limitExceeded) {
4617
+ record.metadata = { ...record.metadata, limitExceeded: true };
4618
+ }
2611
4619
  if (this.anomalyDetector.isEnabled()) {
2612
4620
  this.anomalyDetector.evaluateTransaction(record);
2613
4621
  }
4622
+ this.exporter.export([record]).catch(() => {
4623
+ });
2614
4624
  return record;
2615
4625
  }
2616
4626
  /**
@@ -2629,6 +4639,7 @@ var Kontext = class _Kontext {
2629
4639
  async flush() {
2630
4640
  await this.logger.flush();
2631
4641
  await this.store.flush();
4642
+ await this.exporter.flush();
2632
4643
  }
2633
4644
  /**
2634
4645
  * Restore state from the attached storage adapter.
@@ -2648,6 +4659,7 @@ var Kontext = class _Kontext {
2648
4659
  * @returns The created task
2649
4660
  */
2650
4661
  async createTask(input) {
4662
+ this.validateMetadata(input.metadata);
2651
4663
  return this.taskManager.createTask(input);
2652
4664
  }
2653
4665
  /**
@@ -3033,13 +5045,110 @@ var Kontext = class _Kontext {
3033
5045
  };
3034
5046
  const hash = crypto$1.createHash("sha256");
3035
5047
  hash.update(JSON.stringify(certificateContent));
3036
- const signature = hash.digest("hex");
5048
+ const contentHash = hash.digest("hex");
3037
5049
  return {
3038
5050
  ...certificateContent,
3039
- signature
5051
+ contentHash
3040
5052
  };
3041
5053
  }
3042
5054
  // --------------------------------------------------------------------------
5055
+ // Plan & Usage Metering
5056
+ // --------------------------------------------------------------------------
5057
+ /**
5058
+ * Get current usage statistics for the plan.
5059
+ *
5060
+ * @returns Usage data including plan, event count, limits, and whether the limit is exceeded
5061
+ */
5062
+ getUsage() {
5063
+ return this.planManager.getUsage();
5064
+ }
5065
+ /**
5066
+ * Change the plan tier at runtime (e.g., after Stripe checkout succeeds).
5067
+ *
5068
+ * @param tier - The new plan tier
5069
+ */
5070
+ setPlan(tier) {
5071
+ this.planManager.setPlan(tier);
5072
+ }
5073
+ /**
5074
+ * Register a callback for when usage reaches 80% of the plan limit.
5075
+ *
5076
+ * @param callback - Function to call with the limit event
5077
+ * @returns Unsubscribe function
5078
+ */
5079
+ onUsageWarning(callback) {
5080
+ return this.planManager.onUsageWarning(callback);
5081
+ }
5082
+ /**
5083
+ * Register a callback for when the plan event limit is reached.
5084
+ *
5085
+ * @param callback - Function to call with the limit event
5086
+ * @returns Unsubscribe function
5087
+ */
5088
+ onLimitReached(callback) {
5089
+ return this.planManager.onLimitReached(callback);
5090
+ }
5091
+ /**
5092
+ * Get the URL for upgrading to the Pro plan.
5093
+ *
5094
+ * @returns The upgrade URL (configurable via init)
5095
+ */
5096
+ getUpgradeUrl() {
5097
+ return this.planManager.upgradeUrl;
5098
+ }
5099
+ /**
5100
+ * Get the URL for contacting the team about Enterprise pricing.
5101
+ *
5102
+ * @returns The enterprise contact URL
5103
+ */
5104
+ getEnterpriseContactUrl() {
5105
+ return this.planManager.enterpriseContactUrl;
5106
+ }
5107
+ /**
5108
+ * Persist plan metering state to the storage adapter.
5109
+ * Call this alongside flush() to persist event counts across restarts.
5110
+ */
5111
+ async flushPlanState() {
5112
+ const adapter = this.store.getStorageAdapter();
5113
+ if (!adapter) return;
5114
+ await adapter.save(PLAN_STORAGE_KEY, this.planManager.toJSON());
5115
+ }
5116
+ /**
5117
+ * Restore plan metering state from the storage adapter.
5118
+ * Call this after restore() to reload event counts.
5119
+ */
5120
+ async restorePlanState() {
5121
+ const adapter = this.store.getStorageAdapter();
5122
+ if (!adapter) return;
5123
+ const data = await adapter.load(PLAN_STORAGE_KEY);
5124
+ if (data && typeof data === "object" && data.tier && typeof data.eventCount === "number") {
5125
+ this.planManager.setPlan(data.tier);
5126
+ this.planManager.setEventCount(data.eventCount);
5127
+ if (data.billingPeriodStart) {
5128
+ this.planManager.resetBillingPeriod(new Date(data.billingPeriodStart));
5129
+ this.planManager.setEventCount(data.eventCount);
5130
+ }
5131
+ }
5132
+ }
5133
+ // --------------------------------------------------------------------------
5134
+ // Feature Flags
5135
+ // --------------------------------------------------------------------------
5136
+ /**
5137
+ * Check if a feature flag is enabled for the current environment and plan.
5138
+ * Returns `false` if feature flags are not configured.
5139
+ * Always synchronous — reads from in-memory cache.
5140
+ */
5141
+ isFeatureEnabled(flagName, environment, plan) {
5142
+ if (!this.featureFlagManager) return false;
5143
+ return this.featureFlagManager.isEnabled(flagName, environment, plan);
5144
+ }
5145
+ /**
5146
+ * Get the underlying FeatureFlagManager (or null if not configured).
5147
+ */
5148
+ getFeatureFlagManager() {
5149
+ return this.featureFlagManager;
5150
+ }
5151
+ // --------------------------------------------------------------------------
3043
5152
  // Lifecycle
3044
5153
  // --------------------------------------------------------------------------
3045
5154
  /**
@@ -3047,6 +5156,7 @@ var Kontext = class _Kontext {
3047
5156
  */
3048
5157
  async destroy() {
3049
5158
  await this.logger.destroy();
5159
+ await this.exporter.shutdown();
3050
5160
  }
3051
5161
  };
3052
5162
  var MemoryStorage = class {
@@ -3074,20 +5184,20 @@ var MemoryStorage = class {
3074
5184
  var FileStorage = class {
3075
5185
  baseDir;
3076
5186
  constructor(baseDir) {
3077
- this.baseDir = path2__namespace.resolve(baseDir);
5187
+ this.baseDir = path3__namespace.resolve(baseDir);
3078
5188
  }
3079
5189
  async save(key, data) {
3080
- fs2__namespace.mkdirSync(this.baseDir, { recursive: true });
5190
+ fs3__namespace.mkdirSync(this.baseDir, { recursive: true });
3081
5191
  const filePath = this.keyToPath(key);
3082
- const dir = path2__namespace.dirname(filePath);
3083
- fs2__namespace.mkdirSync(dir, { recursive: true });
3084
- fs2__namespace.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
5192
+ const dir = path3__namespace.dirname(filePath);
5193
+ fs3__namespace.mkdirSync(dir, { recursive: true });
5194
+ fs3__namespace.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
3085
5195
  }
3086
5196
  async load(key) {
3087
5197
  const filePath = this.keyToPath(key);
3088
- if (!fs2__namespace.existsSync(filePath)) return null;
5198
+ if (!fs3__namespace.existsSync(filePath)) return null;
3089
5199
  try {
3090
- const raw = fs2__namespace.readFileSync(filePath, "utf-8");
5200
+ const raw = fs3__namespace.readFileSync(filePath, "utf-8");
3091
5201
  return JSON.parse(raw);
3092
5202
  } catch {
3093
5203
  return null;
@@ -3095,12 +5205,12 @@ var FileStorage = class {
3095
5205
  }
3096
5206
  async delete(key) {
3097
5207
  const filePath = this.keyToPath(key);
3098
- if (fs2__namespace.existsSync(filePath)) {
3099
- fs2__namespace.unlinkSync(filePath);
5208
+ if (fs3__namespace.existsSync(filePath)) {
5209
+ fs3__namespace.unlinkSync(filePath);
3100
5210
  }
3101
5211
  }
3102
5212
  async list(prefix) {
3103
- if (!fs2__namespace.existsSync(this.baseDir)) return [];
5213
+ if (!fs3__namespace.existsSync(this.baseDir)) return [];
3104
5214
  return this.listRecursive(this.baseDir, prefix);
3105
5215
  }
3106
5216
  /** Get the base directory path. */
@@ -3112,18 +5222,18 @@ var FileStorage = class {
3112
5222
  // --------------------------------------------------------------------------
3113
5223
  keyToPath(key) {
3114
5224
  const safeName = key.replace(/[<>"|?*]/g, "_");
3115
- return path2__namespace.join(this.baseDir, `${safeName}.json`);
5225
+ return path3__namespace.join(this.baseDir, `${safeName}.json`);
3116
5226
  }
3117
5227
  pathToKey(filePath) {
3118
- const relative2 = path2__namespace.relative(this.baseDir, filePath);
5228
+ const relative2 = path3__namespace.relative(this.baseDir, filePath);
3119
5229
  return relative2.replace(/\.json$/, "");
3120
5230
  }
3121
5231
  listRecursive(dir, prefix) {
3122
5232
  const keys = [];
3123
- if (!fs2__namespace.existsSync(dir)) return keys;
3124
- const entries = fs2__namespace.readdirSync(dir, { withFileTypes: true });
5233
+ if (!fs3__namespace.existsSync(dir)) return keys;
5234
+ const entries = fs3__namespace.readdirSync(dir, { withFileTypes: true });
3125
5235
  for (const entry of entries) {
3126
- const fullPath = path2__namespace.join(dir, entry.name);
5236
+ const fullPath = path3__namespace.join(dir, entry.name);
3127
5237
  if (entry.isDirectory()) {
3128
5238
  keys.push(...this.listRecursive(fullPath, prefix));
3129
5239
  } else if (entry.isFile() && entry.name.endsWith(".json")) {
@@ -3707,8 +5817,8 @@ var LiveCircleAdapter = class {
3707
5817
  constructor(apiKey) {
3708
5818
  this.apiKey = apiKey;
3709
5819
  }
3710
- async request(method, path3, body) {
3711
- const response = await fetch(`${this.baseUrl}${path3}`, {
5820
+ async request(method, path4, body) {
5821
+ const response = await fetch(`${this.baseUrl}${path4}`, {
3712
5822
  method,
3713
5823
  headers: {
3714
5824
  "Content-Type": "application/json",
@@ -4197,8 +6307,8 @@ var LiveComplianceAdapter = class {
4197
6307
  constructor(apiKey) {
4198
6308
  this.apiKey = apiKey;
4199
6309
  }
4200
- async request(method, path3, body) {
4201
- const response = await fetch(`${this.baseUrl}${path3}`, {
6310
+ async request(method, path4, body) {
6311
+ const response = await fetch(`${this.baseUrl}${path4}`, {
4202
6312
  method,
4203
6313
  headers: {
4204
6314
  "Content-Type": "application/json",
@@ -4565,8 +6675,8 @@ var LiveGasStationAdapter = class {
4565
6675
  constructor(apiKey) {
4566
6676
  this.apiKey = apiKey;
4567
6677
  }
4568
- async request(method, path3, body) {
4569
- const response = await fetch(`${this.baseUrl}${path3}`, {
6678
+ async request(method, path4, body) {
6679
+ const response = await fetch(`${this.baseUrl}${path4}`, {
4570
6680
  method,
4571
6681
  headers: {
4572
6682
  "Content-Type": "application/json",
@@ -4708,8 +6818,6 @@ var GasStationManager = class {
4708
6818
  return NATIVE_TOKENS[chain] ?? "ETH";
4709
6819
  }
4710
6820
  };
4711
-
4712
- // src/webhooks.ts
4713
6821
  var DEFAULT_RETRY_CONFIG = {
4714
6822
  maxRetries: 3,
4715
6823
  baseDelayMs: 1e3,
@@ -4774,15 +6882,25 @@ var WebhookManager = class {
4774
6882
  }
4775
6883
  /**
4776
6884
  * Get all registered webhooks.
6885
+ * Secrets are redacted to prevent accidental exposure in logs or API responses.
4777
6886
  */
4778
6887
  getWebhooks() {
4779
- return Array.from(this.webhooks.values());
6888
+ return Array.from(this.webhooks.values()).map((w) => ({
6889
+ ...w,
6890
+ secret: w.secret ? "***REDACTED***" : w.secret
6891
+ }));
4780
6892
  }
4781
6893
  /**
4782
6894
  * Get a specific webhook by ID.
6895
+ * The secret is redacted to prevent accidental exposure in logs or API responses.
4783
6896
  */
4784
6897
  getWebhook(webhookId) {
4785
- return this.webhooks.get(webhookId);
6898
+ const webhook = this.webhooks.get(webhookId);
6899
+ if (!webhook) return void 0;
6900
+ return {
6901
+ ...webhook,
6902
+ secret: webhook.secret ? "***REDACTED***" : webhook.secret
6903
+ };
4786
6904
  }
4787
6905
  /**
4788
6906
  * Get delivery results for a specific webhook or all webhooks.
@@ -4949,227 +7067,223 @@ var WebhookManager = class {
4949
7067
  };
4950
7068
  }
4951
7069
  async computeSignature(payload, secret) {
4952
- const { createHmac } = await import('crypto');
4953
- const hmac = createHmac("sha256", secret);
7070
+ const hmac = crypto$1.createHmac("sha256", secret);
4954
7071
  hmac.update(JSON.stringify(payload));
4955
7072
  return hmac.digest("hex");
4956
7073
  }
4957
7074
  sleep(ms) {
4958
- return new Promise((resolve2) => setTimeout(resolve2, ms));
7075
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
7076
+ }
7077
+ /**
7078
+ * Verify a webhook signature using constant-time comparison to prevent
7079
+ * timing attacks. Use this in your webhook handler to validate incoming
7080
+ * payloads from Kontext.
7081
+ *
7082
+ * @param payload - The raw JSON payload body (string)
7083
+ * @param signature - The signature from the X-Kontext-Signature header
7084
+ * @param secret - The webhook secret used during registration
7085
+ * @returns Whether the signature is valid
7086
+ *
7087
+ * @example
7088
+ * ```typescript
7089
+ * const isValid = WebhookManager.verifySignature(
7090
+ * req.body, // raw JSON string
7091
+ * req.headers['x-kontext-signature'],
7092
+ * 'my-webhook-secret',
7093
+ * );
7094
+ * if (!isValid) return res.status(401).send('Invalid signature');
7095
+ * ```
7096
+ */
7097
+ static verifySignature(payload, signature, secret) {
7098
+ const hmac = crypto$1.createHmac("sha256", secret);
7099
+ hmac.update(payload);
7100
+ const expected = hmac.digest("hex");
7101
+ if (expected.length !== signature.length) {
7102
+ return false;
7103
+ }
7104
+ return crypto$1.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(signature, "hex"));
4959
7105
  }
4960
7106
  };
4961
7107
 
4962
7108
  // src/integrations/vercel-ai.ts
4963
7109
  function kontextMiddleware(kontext, options) {
4964
- const agentId = options?.agentId ?? "vercel-ai";
4965
- const logToolArgs = options?.logToolArgs ?? false;
4966
- const financialTools = options?.financialTools ?? [];
4967
- const defaultCurrency = options?.defaultCurrency ?? "USDC";
4968
- const trustThreshold = options?.trustThreshold;
4969
- const onBlocked = options?.onBlocked;
7110
+ const cfg = {
7111
+ agentId: options?.agentId ?? "vercel-ai",
7112
+ logToolArgs: options?.logToolArgs ?? false,
7113
+ financialTools: options?.financialTools ?? [],
7114
+ defaultCurrency: options?.defaultCurrency ?? "USDC",
7115
+ trustThreshold: options?.trustThreshold,
7116
+ onBlocked: options?.onBlocked
7117
+ };
4970
7118
  return {
4971
- /**
4972
- * Logs the AI request parameters before the model is invoked.
4973
- * Captures the operation type, model ID, tool count, and generation settings.
4974
- */
4975
- transformParams: async ({ params, type }) => {
4976
- const modelInfo = params["model"];
4977
- const tools = params["tools"];
4978
- await kontext.log({
4979
- type: `ai_${type}`,
4980
- description: `AI ${type} request to ${modelInfo?.modelId ?? "unknown"} model`,
4981
- agentId,
4982
- metadata: {
4983
- model: modelInfo?.modelId ?? "unknown",
4984
- toolCount: Array.isArray(tools) ? tools.length : 0,
4985
- maxTokens: params["maxTokens"] ?? null,
4986
- temperature: params["temperature"] ?? null,
4987
- operationType: type
4988
- }
4989
- });
4990
- return params;
4991
- },
4992
- /**
4993
- * Wraps synchronous generation (`generateText`, `generateObject`).
4994
- * After the model returns, logs every tool call individually and the
4995
- * overall response. For financial tools, automatically creates
4996
- * compliance-tracked transaction records.
4997
- */
4998
- wrapGenerate: async ({
4999
- doGenerate,
5000
- params
5001
- }) => {
5002
- const startTime = Date.now();
5003
- if (trustThreshold !== void 0) {
5004
- const trustScore = await kontext.getTrustScore(agentId);
5005
- if (trustScore.score < trustThreshold) {
5006
- await kontext.log({
5007
- type: "ai_blocked",
5008
- description: `AI generation blocked: agent trust score ${trustScore.score} below threshold ${trustThreshold}`,
5009
- agentId,
5010
- metadata: {
5011
- trustScore: trustScore.score,
5012
- trustLevel: trustScore.level,
5013
- threshold: trustThreshold
5014
- }
5015
- });
5016
- throw new Error(
5017
- `Kontext: AI generation blocked. Agent "${agentId}" trust score (${trustScore.score}) is below the required threshold (${trustThreshold}).`
5018
- );
5019
- }
5020
- }
5021
- const result = await doGenerate();
5022
- const duration = Date.now() - startTime;
5023
- const modelInfo = params["model"];
5024
- const toolCalls = result["toolCalls"];
5025
- if (toolCalls && toolCalls.length > 0) {
5026
- for (const toolCall of toolCalls) {
5027
- if (trustThreshold !== void 0 && financialTools.includes(toolCall.toolName)) {
5028
- const trustScore = await kontext.getTrustScore(agentId);
5029
- if (trustScore.score < trustThreshold) {
5030
- if (onBlocked) {
5031
- onBlocked({ toolName: toolCall.toolName, args: toolCall.args }, `Trust score ${trustScore.score} below threshold ${trustThreshold}`);
5032
- }
5033
- await kontext.log({
5034
- type: "ai_tool_blocked",
5035
- description: `Financial tool "${toolCall.toolName}" blocked: trust score ${trustScore.score} below threshold ${trustThreshold}`,
5036
- agentId,
5037
- metadata: {
5038
- toolName: toolCall.toolName,
5039
- trustScore: trustScore.score,
5040
- threshold: trustThreshold
5041
- }
5042
- });
5043
- continue;
5044
- }
5045
- }
5046
- await kontext.log({
5047
- type: "ai_tool_call",
5048
- description: `Tool call: ${toolCall.toolName}`,
5049
- agentId,
5050
- metadata: {
5051
- toolName: toolCall.toolName,
5052
- args: logToolArgs ? toolCall.args : "[redacted]",
5053
- duration,
5054
- model: modelInfo?.modelId ?? "unknown"
5055
- }
7119
+ transformParams: (ctx) => logTransformParams(kontext, cfg, ctx),
7120
+ wrapGenerate: (ctx) => wrapGenerateWithAudit(kontext, cfg, ctx),
7121
+ wrapStream: (ctx) => wrapStreamWithAudit(kontext, cfg, ctx)
7122
+ };
7123
+ }
7124
+ async function logTransformParams(kontext, cfg, { params, type }) {
7125
+ const modelInfo = params["model"];
7126
+ const tools = params["tools"];
7127
+ await kontext.log({
7128
+ type: `ai_${type}`,
7129
+ description: `AI ${type} request to ${modelInfo?.modelId ?? "unknown"} model`,
7130
+ agentId: cfg.agentId,
7131
+ metadata: {
7132
+ model: modelInfo?.modelId ?? "unknown",
7133
+ toolCount: Array.isArray(tools) ? tools.length : 0,
7134
+ maxTokens: params["maxTokens"] ?? null,
7135
+ temperature: params["temperature"] ?? null,
7136
+ operationType: type
7137
+ }
7138
+ });
7139
+ return params;
7140
+ }
7141
+ async function wrapGenerateWithAudit(kontext, cfg, { doGenerate, params }) {
7142
+ const startTime = Date.now();
7143
+ await enforceAgentTrustThreshold(kontext, cfg);
7144
+ const result = await doGenerate();
7145
+ const duration = Date.now() - startTime;
7146
+ const modelId = extractModelId(params);
7147
+ const toolCalls = result["toolCalls"];
7148
+ if (toolCalls && toolCalls.length > 0) {
7149
+ for (const toolCall of toolCalls) {
7150
+ await processToolCall(kontext, cfg, toolCall, duration, modelId);
7151
+ }
7152
+ }
7153
+ await logGenerateCompletion(kontext, cfg, result, duration, modelId, toolCalls?.length ?? 0);
7154
+ return result;
7155
+ }
7156
+ async function wrapStreamWithAudit(kontext, cfg, { doStream, params }) {
7157
+ const startTime = Date.now();
7158
+ const modelId = extractModelId(params);
7159
+ await kontext.log({
7160
+ type: "ai_stream_start",
7161
+ description: `AI stream started for model ${modelId}`,
7162
+ agentId: cfg.agentId,
7163
+ metadata: { model: modelId, operationType: "stream" }
7164
+ });
7165
+ const { stream, ...rest } = await doStream();
7166
+ const toolCallsInStream = [];
7167
+ const transformedStream = stream.pipeThrough(
7168
+ new TransformStream({
7169
+ transform(chunk, controller) {
7170
+ controller.enqueue(chunk);
7171
+ if (chunk["type"] === "tool-call") {
7172
+ toolCallsInStream.push({
7173
+ toolName: chunk["toolName"],
7174
+ args: chunk["args"]
5056
7175
  });
5057
- if (financialTools.includes(toolCall.toolName)) {
5058
- const amount = extractAmount(toolCall.args);
5059
- if (amount !== null) {
5060
- await kontext.log({
5061
- type: "ai_financial_tool_call",
5062
- description: `Financial tool "${toolCall.toolName}" invoked with amount ${amount} ${defaultCurrency}`,
5063
- agentId,
5064
- metadata: {
5065
- toolName: toolCall.toolName,
5066
- amount: amount.toString(),
5067
- currency: defaultCurrency,
5068
- toolArgs: logToolArgs ? toolCall.args : "[redacted]"
5069
- }
5070
- });
5071
- }
5072
- }
5073
7176
  }
7177
+ },
7178
+ async flush() {
7179
+ const duration = Date.now() - startTime;
7180
+ await logStreamToolCalls(kontext, cfg, toolCallsInStream, duration, modelId);
7181
+ await kontext.log({
7182
+ type: "ai_stream_complete",
7183
+ description: `AI stream completed in ${duration}ms with ${toolCallsInStream.length} tool call(s)`,
7184
+ agentId: cfg.agentId,
7185
+ metadata: { duration, toolCallCount: toolCallsInStream.length, model: modelId }
7186
+ });
5074
7187
  }
5075
- const usage = result["usage"];
5076
- await kontext.log({
5077
- type: "ai_response",
5078
- description: `AI response completed in ${duration}ms`,
5079
- agentId,
5080
- metadata: {
5081
- duration,
5082
- toolCallCount: toolCalls?.length ?? 0,
5083
- finishReason: result["finishReason"] ?? "unknown",
5084
- promptTokens: usage?.promptTokens ?? null,
5085
- completionTokens: usage?.completionTokens ?? null,
5086
- totalTokens: usage?.totalTokens ?? null,
5087
- model: modelInfo?.modelId ?? "unknown"
5088
- }
5089
- });
5090
- return result;
5091
- },
5092
- /**
5093
- * Wraps streaming generation (`streamText`).
5094
- * Pipes the response stream through a transform that monitors for
5095
- * tool call chunks. On stream completion, logs the overall duration
5096
- * and any tool calls that occurred during the stream.
5097
- */
5098
- wrapStream: async ({
5099
- doStream,
5100
- params
5101
- }) => {
5102
- const startTime = Date.now();
5103
- const modelInfo = params["model"];
7188
+ })
7189
+ );
7190
+ return { stream: transformedStream, ...rest };
7191
+ }
7192
+ function extractModelId(params) {
7193
+ return params["model"]?.modelId ?? "unknown";
7194
+ }
7195
+ async function enforceAgentTrustThreshold(kontext, cfg) {
7196
+ if (cfg.trustThreshold === void 0) return;
7197
+ const trustScore = await kontext.getTrustScore(cfg.agentId);
7198
+ if (trustScore.score < cfg.trustThreshold) {
7199
+ await kontext.log({
7200
+ type: "ai_blocked",
7201
+ description: `AI generation blocked: agent trust score ${trustScore.score} below threshold ${cfg.trustThreshold}`,
7202
+ agentId: cfg.agentId,
7203
+ metadata: { trustScore: trustScore.score, trustLevel: trustScore.level, threshold: cfg.trustThreshold }
7204
+ });
7205
+ throw new Error(
7206
+ `Kontext: AI generation blocked. Agent "${cfg.agentId}" trust score (${trustScore.score}) is below the required threshold (${cfg.trustThreshold}).`
7207
+ );
7208
+ }
7209
+ }
7210
+ async function processToolCall(kontext, cfg, toolCall, duration, modelId) {
7211
+ if (cfg.trustThreshold !== void 0 && cfg.financialTools.includes(toolCall.toolName)) {
7212
+ const trustScore = await kontext.getTrustScore(cfg.agentId);
7213
+ if (trustScore.score < cfg.trustThreshold) {
7214
+ cfg.onBlocked?.({ toolName: toolCall.toolName, args: toolCall.args }, `Trust score ${trustScore.score} below threshold ${cfg.trustThreshold}`);
5104
7215
  await kontext.log({
5105
- type: "ai_stream_start",
5106
- description: `AI stream started for model ${modelInfo?.modelId ?? "unknown"}`,
5107
- agentId,
5108
- metadata: {
5109
- model: modelInfo?.modelId ?? "unknown",
5110
- operationType: "stream"
5111
- }
7216
+ type: "ai_tool_blocked",
7217
+ description: `Financial tool "${toolCall.toolName}" blocked: trust score ${trustScore.score} below threshold ${cfg.trustThreshold}`,
7218
+ agentId: cfg.agentId,
7219
+ metadata: { toolName: toolCall.toolName, trustScore: trustScore.score, threshold: cfg.trustThreshold }
5112
7220
  });
5113
- const { stream, ...rest } = await doStream();
5114
- const toolCallsInStream = [];
5115
- const transformedStream = stream.pipeThrough(
5116
- new TransformStream({
5117
- transform(chunk, controller) {
5118
- controller.enqueue(chunk);
5119
- if (chunk["type"] === "tool-call") {
5120
- const toolName = chunk["toolName"];
5121
- const args = chunk["args"];
5122
- toolCallsInStream.push({ toolName, args });
5123
- }
5124
- },
5125
- async flush() {
5126
- const duration = Date.now() - startTime;
5127
- for (const toolCall of toolCallsInStream) {
5128
- await kontext.log({
5129
- type: "ai_tool_call",
5130
- description: `Tool call (stream): ${toolCall.toolName}`,
5131
- agentId,
5132
- metadata: {
5133
- toolName: toolCall.toolName,
5134
- args: logToolArgs ? toolCall.args : "[redacted]",
5135
- duration,
5136
- model: modelInfo?.modelId ?? "unknown",
5137
- source: "stream"
5138
- }
5139
- });
5140
- if (financialTools.includes(toolCall.toolName)) {
5141
- const amount = extractAmount(toolCall.args);
5142
- if (amount !== null) {
5143
- await kontext.log({
5144
- type: "ai_financial_tool_call",
5145
- description: `Financial tool "${toolCall.toolName}" invoked via stream with amount ${amount} ${defaultCurrency}`,
5146
- agentId,
5147
- metadata: {
5148
- toolName: toolCall.toolName,
5149
- amount: amount.toString(),
5150
- currency: defaultCurrency,
5151
- source: "stream"
5152
- }
5153
- });
5154
- }
5155
- }
5156
- }
5157
- await kontext.log({
5158
- type: "ai_stream_complete",
5159
- description: `AI stream completed in ${duration}ms with ${toolCallsInStream.length} tool call(s)`,
5160
- agentId,
5161
- metadata: {
5162
- duration,
5163
- toolCallCount: toolCallsInStream.length,
5164
- model: modelInfo?.modelId ?? "unknown"
5165
- }
5166
- });
5167
- }
5168
- })
5169
- );
5170
- return { stream: transformedStream, ...rest };
7221
+ return;
5171
7222
  }
5172
- };
7223
+ }
7224
+ await kontext.log({
7225
+ type: "ai_tool_call",
7226
+ description: `Tool call: ${toolCall.toolName}`,
7227
+ agentId: cfg.agentId,
7228
+ metadata: {
7229
+ toolName: toolCall.toolName,
7230
+ args: cfg.logToolArgs ? toolCall.args : "[redacted]",
7231
+ duration,
7232
+ model: modelId
7233
+ }
7234
+ });
7235
+ await logFinancialToolCall(kontext, cfg, toolCall);
7236
+ }
7237
+ async function logFinancialToolCall(kontext, cfg, toolCall, source) {
7238
+ if (!cfg.financialTools.includes(toolCall.toolName)) return;
7239
+ const amount = extractAmount(toolCall.args);
7240
+ if (amount === null) return;
7241
+ await kontext.log({
7242
+ type: "ai_financial_tool_call",
7243
+ description: `Financial tool "${toolCall.toolName}" invoked${source ? ` via ${source}` : ""} with amount ${amount} ${cfg.defaultCurrency}`,
7244
+ agentId: cfg.agentId,
7245
+ metadata: {
7246
+ toolName: toolCall.toolName,
7247
+ amount: amount.toString(),
7248
+ currency: cfg.defaultCurrency,
7249
+ ...cfg.logToolArgs ? { toolArgs: toolCall.args } : {},
7250
+ ...source ? { source } : {}
7251
+ }
7252
+ });
7253
+ }
7254
+ async function logGenerateCompletion(kontext, cfg, result, duration, modelId, toolCallCount) {
7255
+ const usage = result["usage"];
7256
+ await kontext.log({
7257
+ type: "ai_response",
7258
+ description: `AI response completed in ${duration}ms`,
7259
+ agentId: cfg.agentId,
7260
+ metadata: {
7261
+ duration,
7262
+ toolCallCount,
7263
+ finishReason: result["finishReason"] ?? "unknown",
7264
+ promptTokens: usage?.promptTokens ?? null,
7265
+ completionTokens: usage?.completionTokens ?? null,
7266
+ totalTokens: usage?.totalTokens ?? null,
7267
+ model: modelId
7268
+ }
7269
+ });
7270
+ }
7271
+ async function logStreamToolCalls(kontext, cfg, toolCalls, duration, modelId) {
7272
+ for (const toolCall of toolCalls) {
7273
+ await kontext.log({
7274
+ type: "ai_tool_call",
7275
+ description: `Tool call (stream): ${toolCall.toolName}`,
7276
+ agentId: cfg.agentId,
7277
+ metadata: {
7278
+ toolName: toolCall.toolName,
7279
+ args: cfg.logToolArgs ? toolCall.args : "[redacted]",
7280
+ duration,
7281
+ model: modelId,
7282
+ source: "stream"
7283
+ }
7284
+ });
7285
+ await logFinancialToolCall(kontext, cfg, toolCall, "stream");
7286
+ }
5173
7287
  }
5174
7288
  function kontextWrapModel(model, kontext, options) {
5175
7289
  const middleware = kontextMiddleware(kontext, options);
@@ -5222,10 +7336,18 @@ function createKontextAI(model, input) {
5222
7336
  return { model: wrappedModel, kontext };
5223
7337
  }
5224
7338
  function withKontext(handler, options) {
7339
+ const resolvedProjectId = options?.projectId ?? process.env["KONTEXT_PROJECT_ID"];
7340
+ if (!resolvedProjectId) {
7341
+ throw new Error("Kontext: projectId is required. Provide it via options or set KONTEXT_PROJECT_ID env var.");
7342
+ }
7343
+ const resolvedApiKey = options?.apiKey ?? process.env["KONTEXT_API_KEY"];
7344
+ if (options?.apiKey !== void 0 && options.apiKey.trim() === "") {
7345
+ throw new Error("Kontext: apiKey was provided but is empty.");
7346
+ }
5225
7347
  const kontext = Kontext.init({
5226
- projectId: options?.projectId ?? process.env["KONTEXT_PROJECT_ID"] ?? "default",
7348
+ projectId: resolvedProjectId,
5227
7349
  environment: options?.environment ?? (process.env["NODE_ENV"] === "production" ? "production" : "development"),
5228
- apiKey: options?.apiKey ?? process.env["KONTEXT_API_KEY"],
7350
+ apiKey: resolvedApiKey,
5229
7351
  debug: options?.debug
5230
7352
  });
5231
7353
  const agentId = options?.agentId ?? "nextjs-route";
@@ -5323,22 +7445,408 @@ function generateRequestId() {
5323
7445
  return `req_${timestamp}-${random}`;
5324
7446
  }
5325
7447
 
7448
+ // src/integrations/cftc-compliance.ts
7449
+ var CFTCCompliance = class {
7450
+ config;
7451
+ collateralValuations = [];
7452
+ segregationCalculations = [];
7453
+ incidents = [];
7454
+ constructor(config) {
7455
+ this.config = {
7456
+ minimumNonBtcEthHaircut: config?.minimumNonBtcEthHaircut ?? 0.2,
7457
+ enableHaircutValidation: config?.enableHaircutValidation ?? true,
7458
+ enableWeeklyReporting: config?.enableWeeklyReporting ?? false,
7459
+ reportingStartDate: config?.reportingStartDate
7460
+ };
7461
+ }
7462
+ // --------------------------------------------------------------------------
7463
+ // Collateral Valuation
7464
+ // --------------------------------------------------------------------------
7465
+ /**
7466
+ * Log a collateral valuation for a digital asset held as customer margin.
7467
+ *
7468
+ * Validates the haircut percentage against CFTC requirements when
7469
+ * `enableHaircutValidation` is true (default).
7470
+ *
7471
+ * @param input - Collateral valuation data (id and timestamp are auto-generated if omitted)
7472
+ * @returns The stored CollateralValuation record
7473
+ * @throws Error if haircut validation fails for the given asset type
7474
+ */
7475
+ logCollateralValuation(input) {
7476
+ if (this.config.enableHaircutValidation) {
7477
+ const validation = this.validateHaircut(input.assetType, input.haircutPercentage);
7478
+ if (!validation.valid) {
7479
+ throw new Error(
7480
+ `Haircut validation failed: ${validation.message}`
7481
+ );
7482
+ }
7483
+ }
7484
+ const valuation = {
7485
+ id: input.id ?? generateId(),
7486
+ timestamp: input.timestamp ?? now(),
7487
+ accountClass: input.accountClass,
7488
+ assetType: input.assetType,
7489
+ assetSymbol: input.assetSymbol,
7490
+ quantity: input.quantity,
7491
+ marketValue: input.marketValue,
7492
+ haircutPercentage: input.haircutPercentage,
7493
+ haircutValue: input.haircutValue,
7494
+ netValue: input.netValue,
7495
+ valuationMethod: input.valuationMethod,
7496
+ agentId: input.agentId,
7497
+ ...input.dcoReference !== void 0 && { dcoReference: input.dcoReference },
7498
+ ...input.metadata !== void 0 && { metadata: input.metadata }
7499
+ };
7500
+ this.collateralValuations.push(valuation);
7501
+ return valuation;
7502
+ }
7503
+ // --------------------------------------------------------------------------
7504
+ // Segregation Calculations
7505
+ // --------------------------------------------------------------------------
7506
+ /**
7507
+ * Log a daily segregation calculation for a given account class.
7508
+ *
7509
+ * @param input - Segregation calculation data (id and timestamp are auto-generated if omitted)
7510
+ * @returns The stored SegregationCalculation record
7511
+ */
7512
+ logSegregationCalculation(input) {
7513
+ const calculation = {
7514
+ id: input.id ?? generateId(),
7515
+ timestamp: input.timestamp ?? now(),
7516
+ accountClass: input.accountClass,
7517
+ totalCustomerFunds: input.totalCustomerFunds,
7518
+ requiredAmount: input.requiredAmount,
7519
+ excessDeficit: input.excessDeficit,
7520
+ digitalAssetBreakdown: input.digitalAssetBreakdown,
7521
+ residualInterest: input.residualInterest,
7522
+ agentId: input.agentId,
7523
+ ...input.metadata !== void 0 && { metadata: input.metadata }
7524
+ };
7525
+ this.segregationCalculations.push(calculation);
7526
+ return calculation;
7527
+ }
7528
+ // --------------------------------------------------------------------------
7529
+ // Incident Reporting
7530
+ // --------------------------------------------------------------------------
7531
+ /**
7532
+ * Log an incident (cybersecurity, operational, system failure, or disruption).
7533
+ *
7534
+ * @param input - Incident data (id and timestamp are auto-generated if omitted)
7535
+ * @returns The stored IncidentReport record
7536
+ */
7537
+ logIncident(input) {
7538
+ const incident = {
7539
+ id: input.id ?? generateId(),
7540
+ timestamp: input.timestamp ?? now(),
7541
+ severity: input.severity,
7542
+ incidentType: input.incidentType,
7543
+ description: input.description,
7544
+ affectedSystems: input.affectedSystems,
7545
+ resolutionStatus: input.resolutionStatus,
7546
+ agentId: input.agentId,
7547
+ ...input.affectedCustomerCount !== void 0 && {
7548
+ affectedCustomerCount: input.affectedCustomerCount
7549
+ },
7550
+ ...input.financialImpact !== void 0 && {
7551
+ financialImpact: input.financialImpact
7552
+ },
7553
+ ...input.metadata !== void 0 && { metadata: input.metadata }
7554
+ };
7555
+ this.incidents.push(incident);
7556
+ return incident;
7557
+ }
7558
+ // --------------------------------------------------------------------------
7559
+ // Report Generation
7560
+ // --------------------------------------------------------------------------
7561
+ /**
7562
+ * Generate a weekly digital asset report aggregating collateral valuations
7563
+ * for a given account class and date range.
7564
+ *
7565
+ * @param accountClass - The CFTC account class to report on
7566
+ * @param periodStart - Start of the reporting period
7567
+ * @param periodEnd - End of the reporting period
7568
+ * @returns DigitalAssetReport with aggregated positions
7569
+ */
7570
+ generateWeeklyDigitalAssetReport(accountClass, periodStart, periodEnd) {
7571
+ const filtered = this.collateralValuations.filter((v) => {
7572
+ const ts = new Date(v.timestamp);
7573
+ return v.accountClass === accountClass && ts >= periodStart && ts <= periodEnd;
7574
+ });
7575
+ const aggregationMap = /* @__PURE__ */ new Map();
7576
+ for (const v of filtered) {
7577
+ const key = `${v.assetType}:${v.assetSymbol}`;
7578
+ const existing = aggregationMap.get(key);
7579
+ if (existing) {
7580
+ existing.totalQuantity += v.quantity;
7581
+ existing.totalMarketValue += v.marketValue;
7582
+ existing.totalHaircutValue += v.haircutValue;
7583
+ existing.totalNetValue += v.netValue;
7584
+ } else {
7585
+ aggregationMap.set(key, {
7586
+ assetType: v.assetType,
7587
+ assetSymbol: v.assetSymbol,
7588
+ totalQuantity: v.quantity,
7589
+ totalMarketValue: v.marketValue,
7590
+ totalHaircutValue: v.haircutValue,
7591
+ totalNetValue: v.netValue
7592
+ });
7593
+ }
7594
+ }
7595
+ const assets = Array.from(aggregationMap.values());
7596
+ const totalMarketValue = assets.reduce((sum, a) => sum + a.totalMarketValue, 0);
7597
+ const totalNetValue = assets.reduce((sum, a) => sum + a.totalNetValue, 0);
7598
+ return {
7599
+ id: generateId(),
7600
+ reportDate: now().split("T")[0],
7601
+ reportPeriodStart: periodStart.toISOString(),
7602
+ reportPeriodEnd: periodEnd.toISOString(),
7603
+ accountClass,
7604
+ assets,
7605
+ totalMarketValue,
7606
+ totalNetValue,
7607
+ generatedAt: now()
7608
+ };
7609
+ }
7610
+ /**
7611
+ * Get the most recent segregation calculation for a given account class
7612
+ * and date.
7613
+ *
7614
+ * @param accountClass - The CFTC account class
7615
+ * @param date - The date to retrieve the calculation for
7616
+ * @returns The most recent SegregationCalculation for that day, or undefined
7617
+ */
7618
+ generateDailySegregationReport(accountClass, date) {
7619
+ const dateStr = date.toISOString().split("T")[0];
7620
+ const matching = this.segregationCalculations.filter((c) => {
7621
+ const cDate = c.timestamp.split("T")[0];
7622
+ return c.accountClass === accountClass && cDate === dateStr;
7623
+ });
7624
+ if (matching.length === 0) return void 0;
7625
+ return matching[matching.length - 1];
7626
+ }
7627
+ // --------------------------------------------------------------------------
7628
+ // Query Methods
7629
+ // --------------------------------------------------------------------------
7630
+ /**
7631
+ * Query collateral valuations with optional filters.
7632
+ *
7633
+ * @param filters - Optional filter criteria
7634
+ * @returns Array of matching CollateralValuation records
7635
+ */
7636
+ getCollateralValuations(filters) {
7637
+ if (!filters) return [...this.collateralValuations];
7638
+ return this.collateralValuations.filter((v) => {
7639
+ if (filters.accountClass && v.accountClass !== filters.accountClass) return false;
7640
+ if (filters.assetType && v.assetType !== filters.assetType) return false;
7641
+ if (filters.startDate) {
7642
+ const ts = new Date(v.timestamp);
7643
+ if (ts < filters.startDate) return false;
7644
+ }
7645
+ if (filters.endDate) {
7646
+ const ts = new Date(v.timestamp);
7647
+ if (ts > filters.endDate) return false;
7648
+ }
7649
+ return true;
7650
+ });
7651
+ }
7652
+ /**
7653
+ * Query incident reports with optional filters.
7654
+ *
7655
+ * @param filters - Optional filter criteria
7656
+ * @returns Array of matching IncidentReport records
7657
+ */
7658
+ getIncidents(filters) {
7659
+ if (!filters) return [...this.incidents];
7660
+ return this.incidents.filter((i) => {
7661
+ if (filters.severity && i.severity !== filters.severity) return false;
7662
+ if (filters.status && i.resolutionStatus !== filters.status) return false;
7663
+ if (filters.startDate) {
7664
+ const ts = new Date(i.timestamp);
7665
+ if (ts < filters.startDate) return false;
7666
+ }
7667
+ if (filters.endDate) {
7668
+ const ts = new Date(i.timestamp);
7669
+ if (ts > filters.endDate) return false;
7670
+ }
7671
+ return true;
7672
+ });
7673
+ }
7674
+ /**
7675
+ * Query segregation calculations with optional filters.
7676
+ *
7677
+ * @param filters - Optional filter criteria
7678
+ * @returns Array of matching SegregationCalculation records
7679
+ */
7680
+ getSegregationCalculations(filters) {
7681
+ if (!filters) return [...this.segregationCalculations];
7682
+ return this.segregationCalculations.filter((c) => {
7683
+ if (filters.accountClass && c.accountClass !== filters.accountClass) return false;
7684
+ if (filters.startDate) {
7685
+ const ts = new Date(c.timestamp);
7686
+ if (ts < filters.startDate) return false;
7687
+ }
7688
+ if (filters.endDate) {
7689
+ const ts = new Date(c.timestamp);
7690
+ if (ts > filters.endDate) return false;
7691
+ }
7692
+ return true;
7693
+ });
7694
+ }
7695
+ // --------------------------------------------------------------------------
7696
+ // Haircut Validation
7697
+ // --------------------------------------------------------------------------
7698
+ /**
7699
+ * Validate a haircut percentage against CFTC Letter 26-05 requirements.
7700
+ *
7701
+ * Rules:
7702
+ * - Payment stablecoins: no minimum haircut required
7703
+ * - BTC / ETH: no minimum (deferred to DCO haircut schedule)
7704
+ * - Other digital assets: minimum 20% haircut required
7705
+ *
7706
+ * @param assetType - The digital asset type
7707
+ * @param haircutPercentage - The proposed haircut (0.0 - 1.0)
7708
+ * @returns HaircutValidationResult with validity, minimum, and message
7709
+ */
7710
+ validateHaircut(assetType, haircutPercentage) {
7711
+ switch (assetType) {
7712
+ case "payment_stablecoin":
7713
+ return {
7714
+ valid: true,
7715
+ minimumRequired: 0,
7716
+ message: "Payment stablecoins have no mandatory minimum haircut per CFTC Letter 26-05."
7717
+ };
7718
+ case "btc":
7719
+ return {
7720
+ valid: true,
7721
+ minimumRequired: 0,
7722
+ message: "BTC haircut deferred to DCO schedule per CFTC Letter 26-05."
7723
+ };
7724
+ case "eth":
7725
+ return {
7726
+ valid: true,
7727
+ minimumRequired: 0,
7728
+ message: "ETH haircut deferred to DCO schedule per CFTC Letter 26-05."
7729
+ };
7730
+ case "other_digital_asset": {
7731
+ const minimum = this.config.minimumNonBtcEthHaircut;
7732
+ const valid = haircutPercentage >= minimum;
7733
+ return {
7734
+ valid,
7735
+ minimumRequired: minimum,
7736
+ message: valid ? `Haircut of ${(haircutPercentage * 100).toFixed(1)}% meets the minimum ${(minimum * 100).toFixed(1)}% requirement for other digital assets.` : `Haircut of ${(haircutPercentage * 100).toFixed(1)}% is below the minimum ${(minimum * 100).toFixed(1)}% required for other digital assets per CFTC Letter 26-05.`
7737
+ };
7738
+ }
7739
+ }
7740
+ }
7741
+ // --------------------------------------------------------------------------
7742
+ // Export
7743
+ // --------------------------------------------------------------------------
7744
+ /**
7745
+ * Export CFTC compliance data in JSON or CSV format.
7746
+ *
7747
+ * @param options - Export options including format, report type, and filters
7748
+ * @returns Formatted string (JSON or CSV)
7749
+ */
7750
+ exportCFTCReport(options) {
7751
+ let data;
7752
+ switch (options.reportType) {
7753
+ case "weekly_digital_assets": {
7754
+ const valuations = this.getCollateralValuations({
7755
+ accountClass: options.accountClass,
7756
+ startDate: options.startDate,
7757
+ endDate: options.endDate
7758
+ });
7759
+ data = valuations.map((v) => ({
7760
+ id: v.id,
7761
+ timestamp: v.timestamp,
7762
+ accountClass: v.accountClass,
7763
+ assetType: v.assetType,
7764
+ assetSymbol: v.assetSymbol,
7765
+ quantity: v.quantity,
7766
+ marketValue: v.marketValue,
7767
+ haircutPercentage: v.haircutPercentage,
7768
+ haircutValue: v.haircutValue,
7769
+ netValue: v.netValue,
7770
+ dcoReference: v.dcoReference ?? "",
7771
+ valuationMethod: v.valuationMethod,
7772
+ agentId: v.agentId
7773
+ }));
7774
+ break;
7775
+ }
7776
+ case "daily_segregation": {
7777
+ const calculations = this.getSegregationCalculations({
7778
+ accountClass: options.accountClass,
7779
+ startDate: options.startDate,
7780
+ endDate: options.endDate
7781
+ });
7782
+ data = calculations.map((c) => ({
7783
+ id: c.id,
7784
+ timestamp: c.timestamp,
7785
+ accountClass: c.accountClass,
7786
+ totalCustomerFunds: c.totalCustomerFunds,
7787
+ requiredAmount: c.requiredAmount,
7788
+ excessDeficit: c.excessDeficit,
7789
+ digitalAssetBreakdown: JSON.stringify(c.digitalAssetBreakdown),
7790
+ residualInterest: c.residualInterest,
7791
+ agentId: c.agentId
7792
+ }));
7793
+ break;
7794
+ }
7795
+ case "incidents": {
7796
+ const incidents = this.getIncidents({
7797
+ startDate: options.startDate,
7798
+ endDate: options.endDate
7799
+ });
7800
+ data = incidents.map((i) => ({
7801
+ id: i.id,
7802
+ timestamp: i.timestamp,
7803
+ severity: i.severity,
7804
+ incidentType: i.incidentType,
7805
+ description: i.description,
7806
+ affectedSystems: i.affectedSystems.join("; "),
7807
+ affectedCustomerCount: i.affectedCustomerCount ?? "",
7808
+ financialImpact: i.financialImpact ?? "",
7809
+ resolutionStatus: i.resolutionStatus,
7810
+ agentId: i.agentId
7811
+ }));
7812
+ break;
7813
+ }
7814
+ }
7815
+ if (options.format === "json") {
7816
+ return JSON.stringify(data, null, 2);
7817
+ }
7818
+ return toCsv(data);
7819
+ }
7820
+ };
7821
+
5326
7822
  exports.CCTPTransferManager = CCTPTransferManager;
7823
+ exports.CFTCCompliance = CFTCCompliance;
5327
7824
  exports.CircleComplianceEngine = CircleComplianceEngine;
5328
7825
  exports.CircleWalletManager = CircleWalletManager;
7826
+ exports.ConsoleExporter = ConsoleExporter;
5329
7827
  exports.DigestChain = DigestChain;
7828
+ exports.FeatureFlagManager = FeatureFlagManager;
5330
7829
  exports.FileStorage = FileStorage;
5331
7830
  exports.GasStationManager = GasStationManager;
7831
+ exports.HttpExporter = HttpExporter;
7832
+ exports.JsonFileExporter = JsonFileExporter;
5332
7833
  exports.Kontext = Kontext;
7834
+ exports.KontextCloudExporter = KontextCloudExporter;
5333
7835
  exports.KontextError = KontextError;
5334
7836
  exports.KontextErrorCode = KontextErrorCode;
5335
7837
  exports.MemoryStorage = MemoryStorage;
7838
+ exports.MultiExporter = MultiExporter;
7839
+ exports.NoopExporter = NoopExporter;
7840
+ exports.OFACSanctionsScreener = OFACSanctionsScreener;
7841
+ exports.PLAN_LIMITS = PLAN_LIMITS;
7842
+ exports.PlanManager = PlanManager;
5336
7843
  exports.UsdcCompliance = UsdcCompliance;
5337
7844
  exports.WebhookManager = WebhookManager;
5338
7845
  exports.createKontextAI = createKontextAI;
5339
7846
  exports.extractAmount = extractAmount;
5340
7847
  exports.kontextMiddleware = kontextMiddleware;
5341
7848
  exports.kontextWrapModel = kontextWrapModel;
7849
+ exports.ofacScreener = ofacScreener;
5342
7850
  exports.verifyExportedChain = verifyExportedChain;
5343
7851
  exports.withKontext = withKontext;
5344
7852
  //# sourceMappingURL=index.js.map