vesant-sdk 1.6.6 → 1.7.0-dev.e0ee6d5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +14 -4
  2. package/dist/{client-ePzhQKp9.d.mts → client-BolQlL5e.d.mts} +1 -1
  3. package/dist/{client-ePzhQKp9.d.ts → client-BolQlL5e.d.ts} +1 -1
  4. package/dist/client-C3DCmGe9.d.ts +436 -0
  5. package/dist/{client-C_A7QLcB.d.ts → client-DMIRx7Tu.d.mts} +5 -3
  6. package/dist/{client-BlCxjbY2.d.mts → client-DoMSYMMR.d.ts} +5 -3
  7. package/dist/client-ZNdnpWe7.d.mts +436 -0
  8. package/dist/compliance/index.d.mts +25 -429
  9. package/dist/compliance/index.d.ts +25 -429
  10. package/dist/compliance/index.js +187 -103
  11. package/dist/compliance/index.js.map +1 -1
  12. package/dist/compliance/index.mjs +187 -104
  13. package/dist/compliance/index.mjs.map +1 -1
  14. package/dist/decisions/index.d.mts +2 -2
  15. package/dist/decisions/index.d.ts +2 -2
  16. package/dist/decisions/index.js +1 -1
  17. package/dist/decisions/index.js.map +1 -1
  18. package/dist/decisions/index.mjs +1 -1
  19. package/dist/decisions/index.mjs.map +1 -1
  20. package/dist/geolocation/index.d.mts +4 -4
  21. package/dist/geolocation/index.d.ts +4 -4
  22. package/dist/geolocation/index.js +7 -24
  23. package/dist/geolocation/index.js.map +1 -1
  24. package/dist/geolocation/index.mjs +7 -24
  25. package/dist/geolocation/index.mjs.map +1 -1
  26. package/dist/index.d.mts +12 -70
  27. package/dist/index.d.ts +12 -70
  28. package/dist/index.js +294 -292
  29. package/dist/index.js.map +1 -1
  30. package/dist/index.mjs +293 -291
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/kyc/core.d.mts +4 -4
  33. package/dist/kyc/core.d.ts +4 -4
  34. package/dist/kyc/core.js +78 -23
  35. package/dist/kyc/core.js.map +1 -1
  36. package/dist/kyc/core.mjs +78 -24
  37. package/dist/kyc/core.mjs.map +1 -1
  38. package/dist/kyc/index.d.mts +269 -45
  39. package/dist/kyc/index.d.ts +269 -45
  40. package/dist/kyc/index.js +78 -23
  41. package/dist/kyc/index.js.map +1 -1
  42. package/dist/kyc/index.mjs +78 -24
  43. package/dist/kyc/index.mjs.map +1 -1
  44. package/dist/react.d.mts +42 -7
  45. package/dist/react.d.ts +42 -7
  46. package/dist/react.js +663 -277
  47. package/dist/react.js.map +1 -1
  48. package/dist/react.mjs +663 -277
  49. package/dist/react.mjs.map +1 -1
  50. package/dist/risk-profile/index.d.mts +4 -4
  51. package/dist/risk-profile/index.d.ts +4 -4
  52. package/dist/risk-profile/index.js +1 -1
  53. package/dist/risk-profile/index.js.map +1 -1
  54. package/dist/risk-profile/index.mjs +1 -1
  55. package/dist/risk-profile/index.mjs.map +1 -1
  56. package/dist/scores/index.d.mts +2 -2
  57. package/dist/scores/index.d.ts +2 -2
  58. package/dist/scores/index.js +1 -1
  59. package/dist/scores/index.js.map +1 -1
  60. package/dist/scores/index.mjs +1 -1
  61. package/dist/scores/index.mjs.map +1 -1
  62. package/dist/tax/index.d.mts +6 -41
  63. package/dist/tax/index.d.ts +6 -41
  64. package/dist/tax/index.js +1 -36
  65. package/dist/tax/index.js.map +1 -1
  66. package/dist/tax/index.mjs +1 -36
  67. package/dist/tax/index.mjs.map +1 -1
  68. package/dist/{types-1RzYeSal.d.mts → types-BOFaMQxI.d.mts} +2 -2
  69. package/dist/{types-B4Ezqo7V.d.mts → types-CBQRNL-l.d.mts} +14 -1
  70. package/dist/{types-B4Ezqo7V.d.ts → types-CBQRNL-l.d.ts} +14 -1
  71. package/dist/{types-X5Md_dD_.d.ts → types-UGyDl1fd.d.ts} +2 -2
  72. package/dist/webhooks/index.d.mts +189 -2
  73. package/dist/webhooks/index.d.ts +189 -2
  74. package/dist/webhooks/index.js +49 -7
  75. package/dist/webhooks/index.js.map +1 -1
  76. package/dist/webhooks/index.mjs +49 -7
  77. package/dist/webhooks/index.mjs.map +1 -1
  78. package/package.json +16 -13
  79. package/dist/fraud/index.d.mts +0 -80
  80. package/dist/fraud/index.d.ts +0 -80
  81. package/dist/fraud/index.js +0 -606
  82. package/dist/fraud/index.js.map +0 -1
  83. package/dist/fraud/index.mjs +0 -604
  84. package/dist/fraud/index.mjs.map +0 -1
  85. package/dist/index-B04H4xfJ.d.mts +0 -320
  86. package/dist/index-CItMPmLL.d.ts +0 -320
package/dist/index.js CHANGED
@@ -242,7 +242,7 @@ var noopLogger = {
242
242
  };
243
243
 
244
244
  // src/core/version.ts
245
- var SDK_VERSION = "1.6.6";
245
+ var SDK_VERSION = "1.7.0";
246
246
 
247
247
  // src/shared/browser-utils.ts
248
248
  function generateUUID() {
@@ -652,8 +652,9 @@ async function computeHmacSha256(message, secret) {
652
652
  const { createHmac } = await import('crypto');
653
653
  return createHmac("sha256", secret).update(message).digest("hex");
654
654
  } catch {
655
- throw new Error(
656
- "No crypto implementation available. Requires Web Crypto API or Node.js crypto module."
655
+ throw new VesantError(
656
+ "No crypto implementation available. Requires Web Crypto API or Node.js crypto module.",
657
+ "CRYPTO_UNAVAILABLE"
657
658
  );
658
659
  }
659
660
  }
@@ -787,7 +788,7 @@ function encodePayload(payload) {
787
788
  } else if (typeof Buffer !== "undefined") {
788
789
  return Buffer.from(json, "utf-8").toString("base64");
789
790
  }
790
- throw new Error("No base64 encoding method available");
791
+ throw new VesantError("No base64 encoding method available", "BASE64_UNAVAILABLE");
791
792
  }
792
793
  async function generateCipherText(options, config) {
793
794
  const warnings = [];
@@ -812,8 +813,9 @@ async function generateCipherText(options, config) {
812
813
  if (location) {
813
814
  locationData = location;
814
815
  } else if (gpsRequiredByConfig) {
815
- throw new Error(
816
- `GPS location is required for ${options.reason} by tenant configuration, but GPS was not available or permission was denied`
816
+ throw new VesantError(
817
+ `GPS location is required for ${options.reason} by tenant configuration, but GPS was not available or permission was denied`,
818
+ "GPS_REQUIRED"
817
819
  );
818
820
  } else {
819
821
  warnings.push("GPS location not available or permission denied");
@@ -932,10 +934,9 @@ var GeolocationClient = class extends BaseClient {
932
934
  if (!request.ip_address?.trim()) {
933
935
  throw new ValidationError("ip_address is required and must be a non-empty string", ["ip_address"]);
934
936
  }
935
- const enrichedRequest = request.device_fingerprint ? request : { ...request, device_fingerprint: collectDeviceFingerprint() };
936
937
  return this.requestWithRetry("/api/v1/geo/verify", {
937
938
  method: "POST",
938
- body: JSON.stringify(enrichedRequest)
939
+ body: JSON.stringify(request)
939
940
  }, void 0, void 0, requestOptions);
940
941
  }
941
942
  /**
@@ -1160,6 +1161,7 @@ var GeolocationClient = class extends BaseClient {
1160
1161
  risk_level: risk.level ?? "low",
1161
1162
  risk_score: risk.score ?? 0,
1162
1163
  risk_reasons: risk.is_blocked && risk.block_reasons ? risk.block_reasons : risk.factors ?? [],
1164
+ risk_reasons_structured: risk.block_reasons_structured ?? [],
1163
1165
  jurisdiction: cipherTextResult.jurisdiction,
1164
1166
  geofence_evaluation: cipherTextResult.geofence_evaluation,
1165
1167
  record_id: cipherTextResult.record_id ?? "",
@@ -1339,24 +1341,6 @@ var GeolocationClient = class extends BaseClient {
1339
1341
  // Utility Methods (inherited from BaseClient: healthCheck, updateConfig, getConfig, buildQueryString)
1340
1342
  // ============================================================================
1341
1343
  };
1342
- function collectDeviceFingerprint() {
1343
- const deviceId = generateDeviceId();
1344
- const browserInfo = getBrowserInfo();
1345
- const userAgent = typeof navigator !== "undefined" ? navigator.userAgent : "server";
1346
- const platform = typeof navigator !== "undefined" ? navigator.platform || "unknown" : "server";
1347
- return {
1348
- device_id: deviceId,
1349
- user_agent: userAgent,
1350
- platform,
1351
- browser: browserInfo.browser,
1352
- browser_version: browserInfo.browser_version,
1353
- os: browserInfo.os,
1354
- os_version: browserInfo.os_version,
1355
- screen_resolution: typeof screen !== "undefined" ? `${screen.width}x${screen.height}` : void 0,
1356
- language: typeof navigator !== "undefined" ? navigator.language : void 0,
1357
- timezone: Intl?.DateTimeFormat?.()?.resolvedOptions?.()?.timeZone
1358
- };
1359
- }
1360
1344
 
1361
1345
  // src/risk-profile/client.ts
1362
1346
  var RiskProfileClient = class extends BaseClient {
@@ -1473,6 +1457,81 @@ var RiskProfileClient = class extends BaseClient {
1473
1457
  }
1474
1458
  };
1475
1459
 
1460
+ // src/compliance/block-reasons.ts
1461
+ var sdkReasons = {
1462
+ // Jurisdiction
1463
+ jurisdictionBlocked: (country, countryISO) => ({
1464
+ code: "JURISDICTION_BLOCKED",
1465
+ message: "Access from a blocked jurisdiction",
1466
+ metadata: { country, country_iso: countryISO }
1467
+ }),
1468
+ jurisdictionNonCompliant: () => ({
1469
+ code: "JURISDICTION_NON_COMPLIANT",
1470
+ message: "Location does not meet compliance requirements"
1471
+ }),
1472
+ jurisdictionRegistrationDenied: () => ({
1473
+ code: "JURISDICTION_REGISTRATION_DENIED",
1474
+ message: "Registration is not permitted in this jurisdiction"
1475
+ }),
1476
+ jurisdictionRestricted: () => ({
1477
+ code: "JURISDICTION_RESTRICTED",
1478
+ message: "Access from a restricted jurisdiction"
1479
+ }),
1480
+ // Risk
1481
+ riskCriticalLevel: () => ({
1482
+ code: "RISK_CRITICAL_LEVEL",
1483
+ message: "Risk assessment reached critical threshold"
1484
+ }),
1485
+ accountSuspended: () => ({
1486
+ code: "RISK_ACCOUNT_SUSPENDED",
1487
+ message: "Customer account is suspended"
1488
+ }),
1489
+ sanctionsMatch: () => ({
1490
+ code: "RISK_SANCTIONS_MATCH",
1491
+ message: "Customer profile matched against sanctions list"
1492
+ }),
1493
+ riskHighLocation: () => ({
1494
+ code: "RISK_HIGH_LOCATION",
1495
+ message: "High-risk geographic location"
1496
+ }),
1497
+ riskHighCustomer: () => ({
1498
+ code: "RISK_HIGH_CUSTOMER",
1499
+ message: "Customer flagged as high risk"
1500
+ }),
1501
+ // Device
1502
+ ciphertextInvalid: () => ({
1503
+ code: "DEVICE_CIPHERTEXT_INVALID",
1504
+ message: "Device verification payload failed validation"
1505
+ }),
1506
+ gpsIPMismatch: () => ({
1507
+ code: "DEVICE_GPS_IP_MISMATCH",
1508
+ message: "GPS location does not match IP-derived location"
1509
+ }),
1510
+ gpsRequired: () => ({
1511
+ code: "DEVICE_GPS_REQUIRED",
1512
+ message: "GPS verification is required but was not provided"
1513
+ }),
1514
+ // Transaction
1515
+ transactionHighAmount: (amount, currency, threshold) => ({
1516
+ code: "TRANSACTION_HIGH_AMOUNT",
1517
+ message: "Transaction amount exceeds high-value threshold",
1518
+ metadata: { amount, currency, threshold }
1519
+ }),
1520
+ transactionElevatedAmount: (amount, currency, threshold) => ({
1521
+ code: "TRANSACTION_ELEVATED_AMOUNT",
1522
+ message: "Transaction amount exceeds elevated-value threshold",
1523
+ metadata: { amount, currency, threshold }
1524
+ }),
1525
+ transactionJurisdictionLimit: () => ({
1526
+ code: "TRANSACTION_JURISDICTION_LIMIT",
1527
+ message: "Transaction amount exceeds jurisdiction limit"
1528
+ }),
1529
+ anonymizationDetected: () => ({
1530
+ code: "NETWORK_ANONYMIZER_DETECTED",
1531
+ message: "Anonymization tool detected"
1532
+ })
1533
+ };
1534
+
1476
1535
  // src/compliance/types.ts
1477
1536
  var DEFAULT_CURRENCY_RATES = {
1478
1537
  USD: 1,
@@ -1619,10 +1678,10 @@ var ComplianceClient = class {
1619
1678
  device_fingerprint: request.deviceFingerprint
1620
1679
  }, requestOptions);
1621
1680
  }
1622
- const blockReasons = this.evaluateRegistrationBlock(geoVerification, cipherTextResult);
1623
- if (blockReasons.length > 0) {
1681
+ const structuredBlockReasons = this.evaluateRegistrationBlock(geoVerification, cipherTextResult);
1682
+ if (structuredBlockReasons.length > 0) {
1624
1683
  if (this.config.debug) {
1625
- this.logger.debug("Registration blocked at geo stage", { blockReasons });
1684
+ this.logger.debug("Registration blocked at geo stage", { blockReasons: structuredBlockReasons });
1626
1685
  }
1627
1686
  return {
1628
1687
  allowed: false,
@@ -1630,9 +1689,7 @@ var ComplianceClient = class {
1630
1689
  profile: null,
1631
1690
  requiresKYC: false,
1632
1691
  requiresEDD: false,
1633
- blockReasons,
1634
- processingTime: Date.now() - startTime,
1635
- cipherTextValidation: cipherTextResult
1692
+ ...this.buildReasonTail(structuredBlockReasons, startTime, cipherTextResult)
1636
1693
  };
1637
1694
  }
1638
1695
  const profile = await this.riskClient.createProfile({
@@ -1666,14 +1723,12 @@ var ComplianceClient = class {
1666
1723
  profile,
1667
1724
  requiresKYC,
1668
1725
  requiresEDD,
1669
- blockReasons: [],
1670
- processingTime: Date.now() - startTime,
1671
- cipherTextValidation: cipherTextResult
1726
+ ...this.buildReasonTail([], startTime, cipherTextResult)
1672
1727
  };
1673
1728
  } catch (error) {
1674
1729
  if (this.config.debug) {
1675
1730
  this.logger.error("Registration verification failed", {
1676
- code: error instanceof Error ? error.code : void 0,
1731
+ code: error instanceof VesantError ? error.code : void 0,
1677
1732
  message: error instanceof Error ? error.message : "Unknown error"
1678
1733
  });
1679
1734
  }
@@ -1702,52 +1757,62 @@ var ComplianceClient = class {
1702
1757
  evaluateRegistrationBlock(geoVerification, cipherTextResult) {
1703
1758
  const blockReasons = [];
1704
1759
  if (geoVerification.is_blocked) {
1705
- blockReasons.push(...geoVerification.risk_reasons ?? []);
1760
+ blockReasons.push(...geoVerification.risk_reasons_structured ?? []);
1706
1761
  }
1707
1762
  if (!geoVerification.is_compliant) {
1708
- if (!blockReasons.includes("non_compliant_jurisdiction")) {
1709
- blockReasons.push("non_compliant_jurisdiction");
1763
+ if (!blockReasons.some((r) => r.code === "JURISDICTION_NON_COMPLIANT")) {
1764
+ blockReasons.push(sdkReasons.jurisdictionNonCompliant());
1710
1765
  }
1711
1766
  }
1712
1767
  const jurisdiction = geoVerification.jurisdiction;
1713
1768
  if (jurisdiction) {
1714
1769
  if (jurisdiction.allow_registration === false) {
1715
- blockReasons.push("registration_not_allowed_in_jurisdiction");
1770
+ blockReasons.push(sdkReasons.jurisdictionRegistrationDenied());
1716
1771
  }
1717
1772
  if (jurisdiction.status === "blocked" || jurisdiction.status === "sanctioned") {
1718
- if (!blockReasons.includes("blocked_jurisdiction")) {
1719
- blockReasons.push("blocked_jurisdiction");
1773
+ if (!blockReasons.some((r) => r.code === "JURISDICTION_BLOCKED")) {
1774
+ blockReasons.push(sdkReasons.jurisdictionBlocked(jurisdiction.country_name ?? "", jurisdiction.country_iso ?? ""));
1720
1775
  }
1721
1776
  }
1722
1777
  if (jurisdiction.status === "restricted" && jurisdiction.allow_registration === false) {
1723
- if (!blockReasons.includes("restricted_jurisdiction_no_registration")) {
1724
- blockReasons.push("restricted_jurisdiction_no_registration");
1778
+ if (!blockReasons.some((r) => r.code === "JURISDICTION_RESTRICTED")) {
1779
+ blockReasons.push(sdkReasons.jurisdictionRestricted());
1725
1780
  }
1726
1781
  }
1727
1782
  }
1728
1783
  if (geoVerification.geofence_evaluation?.blocked) {
1729
- blockReasons.push(...geoVerification.geofence_evaluation.reasons ?? []);
1784
+ blockReasons.push(
1785
+ ...(geoVerification.geofence_evaluation.reasons ?? []).map((m) => ({
1786
+ code: "JURISDICTION_GEOFENCE_VIOLATION",
1787
+ message: m
1788
+ }))
1789
+ );
1730
1790
  }
1731
1791
  if (geoVerification.risk_level === "critical") {
1732
- if (!blockReasons.includes("critical_risk_level")) {
1733
- blockReasons.push("critical_risk_level");
1792
+ if (!blockReasons.some((r) => r.code === "RISK_CRITICAL_LEVEL")) {
1793
+ blockReasons.push(sdkReasons.riskCriticalLevel());
1734
1794
  }
1735
1795
  }
1736
1796
  if (cipherTextResult) {
1737
1797
  if (!cipherTextResult.valid) {
1738
- blockReasons.push("ciphertext_validation_failed");
1798
+ blockReasons.push(sdkReasons.ciphertextInvalid());
1739
1799
  }
1740
1800
  if (cipherTextResult.risk?.is_blocked) {
1741
- blockReasons.push(...cipherTextResult.risk.block_reasons || []);
1801
+ blockReasons.push(...cipherTextResult.risk.block_reasons_structured ?? []);
1742
1802
  }
1743
1803
  if (cipherTextResult.risk?.location_mismatch) {
1744
- blockReasons.push("gps_ip_location_mismatch");
1804
+ blockReasons.push(sdkReasons.gpsIPMismatch());
1745
1805
  }
1746
1806
  }
1747
1807
  if (geoVerification.gps_required && !cipherTextResult) {
1748
- blockReasons.push("gps_verification_required");
1808
+ blockReasons.push(sdkReasons.gpsRequired());
1749
1809
  }
1750
- return [...new Set(blockReasons)];
1810
+ const seen = /* @__PURE__ */ new Set();
1811
+ return blockReasons.filter((r) => {
1812
+ if (seen.has(r.code)) return false;
1813
+ seen.add(r.code);
1814
+ return true;
1815
+ });
1751
1816
  }
1752
1817
  /**
1753
1818
  * Validate registration request has all required fields
@@ -1918,9 +1983,7 @@ var ComplianceClient = class {
1918
1983
  geolocation: geoVerification,
1919
1984
  profile: null,
1920
1985
  requiresStepUp: false,
1921
- blockReasons: loginBlockReasons,
1922
- processingTime: Date.now() - startTime,
1923
- cipherTextValidation: cipherTextResult
1986
+ ...this.buildReasonTail(loginBlockReasons, startTime, cipherTextResult)
1924
1987
  };
1925
1988
  }
1926
1989
  let profile;
@@ -1940,14 +2003,13 @@ var ComplianceClient = class {
1940
2003
  profile = await this.createProfileFromGeo(request.customerId, geoVerification);
1941
2004
  }
1942
2005
  const requiresStepUp = geoVerification.risk_level === "high" || geoVerification.risk_level === "critical" || cipherTextResult?.risk?.location_mismatch === true;
2006
+ const loginFinalStructured = isBlocked || profile.customer_status === "suspended" ? this.getBlockReasons(geoVerification, profile, cipherTextResult) : [];
1943
2007
  return {
1944
2008
  allowed: !isBlocked && profile.customer_status !== "suspended",
1945
2009
  geolocation: geoVerification,
1946
2010
  profile,
1947
2011
  requiresStepUp,
1948
- blockReasons: isBlocked || profile.customer_status === "suspended" ? this.getBlockReasons(geoVerification, profile, cipherTextResult) : [],
1949
- processingTime: Date.now() - startTime,
1950
- cipherTextValidation: cipherTextResult
2012
+ ...this.buildReasonTail(loginFinalStructured, startTime, cipherTextResult)
1951
2013
  };
1952
2014
  } catch (error) {
1953
2015
  throw new ComplianceError("Login verification failed", error instanceof Error ? error.message : void 0);
@@ -2029,20 +2091,19 @@ var ComplianceClient = class {
2029
2091
  }
2030
2092
  const geoBlocked = !geoVerification.is_compliant || geoVerification.is_blocked || !!cipherTextResult?.risk?.is_blocked || cipherTextResult?.valid === false || geoVerification.gps_required && !cipherTextResult;
2031
2093
  if (geoBlocked && !profileResult) {
2094
+ const earlyStructured = this.getTransactionBlockReasons(
2095
+ geoVerification,
2096
+ { score: 0, level: "low", factors: [], allowed: false, requiresManualReview: false },
2097
+ true,
2098
+ cipherTextResult
2099
+ );
2032
2100
  return {
2033
2101
  allowed: false,
2034
2102
  geolocation: geoVerification,
2035
2103
  profile: null,
2036
2104
  transactionRisk: { score: 0, level: "low", factors: [], allowed: false, requiresManualReview: false },
2037
2105
  requiresApproval: false,
2038
- blockReasons: this.getTransactionBlockReasons(
2039
- geoVerification,
2040
- { score: 0, level: "low", factors: [], allowed: false, requiresManualReview: false },
2041
- true,
2042
- cipherTextResult
2043
- ),
2044
- processingTime: Date.now() - startTime,
2045
- cipherTextValidation: cipherTextResult
2106
+ ...this.buildReasonTail(earlyStructured, startTime, cipherTextResult)
2046
2107
  };
2047
2108
  }
2048
2109
  if (!profileResult) {
@@ -2062,20 +2123,19 @@ var ComplianceClient = class {
2062
2123
  geoVerification.jurisdiction
2063
2124
  );
2064
2125
  const isAllowed = !geoBlocked && jurisdictionAllowed && transactionRisk.allowed && profile.customer_status !== "suspended";
2126
+ const txStructured = this.getTransactionBlockReasons(
2127
+ geoVerification,
2128
+ transactionRisk,
2129
+ jurisdictionAllowed,
2130
+ cipherTextResult
2131
+ );
2065
2132
  return {
2066
2133
  allowed: isAllowed,
2067
2134
  geolocation: geoVerification,
2068
2135
  profile,
2069
2136
  transactionRisk,
2070
2137
  requiresApproval: transactionRisk.requiresManualReview,
2071
- blockReasons: this.getTransactionBlockReasons(
2072
- geoVerification,
2073
- transactionRisk,
2074
- jurisdictionAllowed,
2075
- cipherTextResult
2076
- ),
2077
- processingTime: Date.now() - startTime,
2078
- cipherTextValidation: cipherTextResult
2138
+ ...this.buildReasonTail(txStructured, startTime, cipherTextResult)
2079
2139
  };
2080
2140
  } catch (error) {
2081
2141
  throw new ComplianceError("Transaction verification failed", error instanceof Error ? error.message : void 0);
@@ -2128,30 +2188,42 @@ var ComplianceClient = class {
2128
2188
  }
2129
2189
  }
2130
2190
  const cipherTextBlocked = cipherTextResult ? !cipherTextResult.valid || cipherTextResult.risk?.is_blocked === true : false;
2131
- const blockReasons = [...geoVerification.risk_reasons ?? []];
2191
+ const structuredEventReasons = [...geoVerification.risk_reasons_structured ?? []];
2132
2192
  if (cipherTextResult) {
2133
2193
  if (!cipherTextResult.valid) {
2134
- blockReasons.push("ciphertext_validation_failed");
2194
+ structuredEventReasons.push(sdkReasons.ciphertextInvalid());
2135
2195
  }
2136
2196
  if (cipherTextResult.risk?.is_blocked) {
2137
- blockReasons.push(...cipherTextResult.risk.block_reasons || []);
2197
+ structuredEventReasons.push(...cipherTextResult.risk.block_reasons_structured ?? []);
2138
2198
  }
2139
2199
  }
2140
2200
  const gpsBlocked = geoVerification.gps_required && !cipherTextResult;
2141
2201
  if (gpsBlocked) {
2142
- blockReasons.push("gps_verification_required");
2202
+ structuredEventReasons.push(sdkReasons.gpsRequired());
2143
2203
  }
2204
+ const seen = /* @__PURE__ */ new Set();
2205
+ const dedupedStructured = structuredEventReasons.filter((r) => {
2206
+ if (seen.has(r.code)) return false;
2207
+ seen.add(r.code);
2208
+ return true;
2209
+ });
2144
2210
  return {
2145
2211
  allowed: geoVerification.is_compliant && !geoVerification.is_blocked && !cipherTextBlocked && !gpsBlocked,
2146
2212
  geolocation: geoVerification,
2147
- blockReasons: [...new Set(blockReasons)],
2148
- processingTime: Date.now() - startTime,
2149
- cipherTextValidation: cipherTextResult
2213
+ ...this.buildReasonTail(dedupedStructured, startTime, cipherTextResult)
2150
2214
  };
2151
2215
  }
2152
2216
  // ============================================================================
2153
2217
  // Helper Methods
2154
2218
  // ============================================================================
2219
+ buildReasonTail(structured, startTime, cipherTextResult) {
2220
+ return {
2221
+ blockReasons: structured.map((r) => r.message),
2222
+ blockReasonsStructured: structured,
2223
+ processingTime: Date.now() - startTime,
2224
+ cipherTextValidation: cipherTextResult
2225
+ };
2226
+ }
2155
2227
  shouldUpdateProfile(profile, newCity) {
2156
2228
  return !profile.location || !profile.location.includes(newCity);
2157
2229
  }
@@ -2228,7 +2300,8 @@ var ComplianceClient = class {
2228
2300
  is_blocked: risk.is_blocked,
2229
2301
  risk_level: risk.level,
2230
2302
  risk_score: risk.score,
2231
- risk_reasons: risk.is_blocked && risk.block_reasons ? risk.block_reasons : risk.factors ?? [],
2303
+ risk_reasons: [],
2304
+ risk_reasons_structured: risk.is_blocked && risk.block_reasons_structured ? risk.block_reasons_structured : [],
2232
2305
  jurisdiction: ct.jurisdiction,
2233
2306
  geofence_evaluation: ct.geofence_evaluation,
2234
2307
  record_id: ct.record_id ?? "",
@@ -2243,35 +2316,35 @@ var ComplianceClient = class {
2243
2316
  );
2244
2317
  }
2245
2318
  let riskScore = 0;
2246
- const factors = [];
2319
+ const structuredFactors = [];
2247
2320
  const normalizedAmount = this.normalizeToUSD(amount, currency);
2248
2321
  if (normalizedAmount > 1e4) {
2249
2322
  riskScore += 30;
2250
- factors.push("high_transaction_amount");
2323
+ structuredFactors.push(sdkReasons.transactionHighAmount(normalizedAmount, currency, 1e4));
2251
2324
  } else if (normalizedAmount > 5e3) {
2252
2325
  riskScore += 15;
2253
- factors.push("elevated_transaction_amount");
2326
+ structuredFactors.push(sdkReasons.transactionElevatedAmount(normalizedAmount, currency, 5e3));
2254
2327
  }
2255
2328
  riskScore += geoVerification.risk_score * 0.4;
2256
2329
  if (geoVerification.risk_level === "high" || geoVerification.risk_level === "critical") {
2257
- factors.push("high_risk_location");
2330
+ structuredFactors.push(sdkReasons.riskHighLocation());
2258
2331
  }
2259
2332
  riskScore += profile.risk_score * 0.3;
2260
2333
  if (profile.risk_category === "high" || profile.risk_category === "critical") {
2261
- factors.push("high_risk_customer");
2334
+ structuredFactors.push(sdkReasons.riskHighCustomer());
2262
2335
  }
2263
2336
  if (geoVerification.location.is_vpn || geoVerification.location.is_proxy) {
2264
2337
  riskScore += 20;
2265
- factors.push("anonymization_detected");
2338
+ structuredFactors.push(sdkReasons.anonymizationDetected());
2266
2339
  }
2267
2340
  if (profile.customer_status === "suspended") {
2268
2341
  riskScore += 50;
2269
- factors.push("account_suspended");
2342
+ structuredFactors.push(sdkReasons.accountSuspended());
2270
2343
  }
2271
2344
  if (cipherTextResult) {
2272
2345
  if (cipherTextResult.risk?.location_mismatch) {
2273
2346
  riskScore += 20;
2274
- factors.push("gps_ip_location_mismatch");
2347
+ structuredFactors.push(sdkReasons.gpsIPMismatch());
2275
2348
  }
2276
2349
  if (cipherTextResult.risk?.score) {
2277
2350
  riskScore += cipherTextResult.risk.score * 0.2;
@@ -2280,7 +2353,8 @@ var ComplianceClient = class {
2280
2353
  return {
2281
2354
  score: Math.min(riskScore, 100),
2282
2355
  level: this.getRiskLevel(riskScore),
2283
- factors,
2356
+ factors: structuredFactors.map((r) => r.message),
2357
+ factorsStructured: structuredFactors,
2284
2358
  allowed: riskScore < 70,
2285
2359
  requiresManualReview: riskScore >= 60 && riskScore < 70
2286
2360
  };
@@ -2305,52 +2379,62 @@ var ComplianceClient = class {
2305
2379
  getBlockReasons(geoVerification, profile, cipherTextResult) {
2306
2380
  const reasons = [];
2307
2381
  if (geoVerification.is_blocked) {
2308
- reasons.push(...geoVerification.risk_reasons ?? []);
2382
+ reasons.push(...geoVerification.risk_reasons_structured ?? []);
2309
2383
  }
2310
2384
  if (profile) {
2311
2385
  if (profile.customer_status === "suspended") {
2312
- reasons.push("account_suspended");
2386
+ reasons.push(sdkReasons.accountSuspended());
2313
2387
  }
2314
2388
  if (profile.has_sanctions) {
2315
- reasons.push("sanctions_match");
2389
+ reasons.push(sdkReasons.sanctionsMatch());
2316
2390
  }
2317
2391
  }
2318
2392
  if (cipherTextResult) {
2319
2393
  if (!cipherTextResult.valid) {
2320
- reasons.push("ciphertext_validation_failed");
2394
+ reasons.push(sdkReasons.ciphertextInvalid());
2321
2395
  }
2322
2396
  if (cipherTextResult.risk?.is_blocked) {
2323
- reasons.push(...cipherTextResult.risk.block_reasons || []);
2397
+ reasons.push(...cipherTextResult.risk.block_reasons_structured ?? []);
2324
2398
  }
2325
2399
  }
2326
2400
  if (geoVerification.gps_required && !cipherTextResult) {
2327
- reasons.push("gps_verification_required");
2401
+ reasons.push(sdkReasons.gpsRequired());
2328
2402
  }
2329
- return [...new Set(reasons)];
2403
+ const seen = /* @__PURE__ */ new Set();
2404
+ return reasons.filter((r) => {
2405
+ if (seen.has(r.code)) return false;
2406
+ seen.add(r.code);
2407
+ return true;
2408
+ });
2330
2409
  }
2331
2410
  getTransactionBlockReasons(geoVerification, transactionRisk, jurisdictionAllowed, cipherTextResult) {
2332
2411
  const reasons = [];
2333
2412
  if (!geoVerification.is_compliant) {
2334
- reasons.push("non_compliant_jurisdiction");
2413
+ reasons.push(sdkReasons.jurisdictionNonCompliant());
2335
2414
  }
2336
2415
  if (!jurisdictionAllowed) {
2337
- reasons.push("exceeds_jurisdiction_limit");
2416
+ reasons.push(sdkReasons.transactionJurisdictionLimit());
2338
2417
  }
2339
2418
  if (!transactionRisk.allowed) {
2340
- reasons.push(...transactionRisk.factors);
2419
+ reasons.push(...transactionRisk.factorsStructured ?? []);
2341
2420
  }
2342
2421
  if (cipherTextResult) {
2343
2422
  if (!cipherTextResult.valid) {
2344
- reasons.push("ciphertext_validation_failed");
2423
+ reasons.push(sdkReasons.ciphertextInvalid());
2345
2424
  }
2346
2425
  if (cipherTextResult.risk?.is_blocked) {
2347
- reasons.push(...cipherTextResult.risk.block_reasons || []);
2426
+ reasons.push(...cipherTextResult.risk.block_reasons_structured ?? []);
2348
2427
  }
2349
2428
  }
2350
2429
  if (geoVerification.gps_required && !cipherTextResult) {
2351
- reasons.push("gps_verification_required");
2430
+ reasons.push(sdkReasons.gpsRequired());
2352
2431
  }
2353
- return [...new Set(reasons)];
2432
+ const seen = /* @__PURE__ */ new Set();
2433
+ return reasons.filter((r) => {
2434
+ if (seen.has(r.code)) return false;
2435
+ seen.add(r.code);
2436
+ return true;
2437
+ });
2354
2438
  }
2355
2439
  // ============================================================================
2356
2440
  // Location Request Methods
@@ -2628,23 +2712,19 @@ var KycClient = class extends BaseClient {
2628
2712
  *
2629
2713
  * Generates a link that the user can visit to submit their KYC documents.
2630
2714
  *
2631
- * @param request - Request containing the user ID, redirect URL, callback URL, and trigger event
2632
- * @returns Response containing kyc_required, can_skip, and optionally the redirect link and KYC ID
2715
+ * @param request - Request containing the user ID, optional redirect URL, and optional callback URL (receives POST requests)
2716
+ * @returns Response containing the redirect link and KYC ID
2633
2717
  *
2634
2718
  * @example
2635
2719
  * ```typescript
2636
2720
  * const result = await client.requestKycSubmitLink({
2637
2721
  * user_id: "user_123",
2638
- * redirect_url: "https://merchant.com/kyc-complete",
2639
- * callback_url: "https://merchant.com/api/kyc-webhook",
2640
- * trigger_event: "onboarding"
2722
+ * redirect_url: "https://merchant.com/kyc-complete", // optional
2723
+ * callback_url: "https://merchant.com/api/kyc-webhook" // optional - receives POST requests on status change
2641
2724
  * });
2642
2725
  *
2643
- * if (result.kyc_required && result.link) {
2644
- * console.log(`Redirect user to: ${result.link}`);
2645
- * } else if (result.can_skip) {
2646
- * console.log("KYC not required, user can proceed");
2647
- * }
2726
+ * console.log(`Redirect user to: ${result.link}`);
2727
+ * console.log(`KYC ID: ${result.kyc_id}`);
2648
2728
  * ```
2649
2729
  */
2650
2730
  async requestKycSubmitLink(request) {
@@ -2655,40 +2735,84 @@ var KycClient = class extends BaseClient {
2655
2735
  });
2656
2736
  }
2657
2737
  /**
2658
- * Create a reuse KYC session for validate a user with existing KYC verification
2738
+ * Create a Re-Use KYC session.
2739
+ *
2740
+ * Inspect the response before showing UI:
2741
+ * - `is_required === false` → skip face capture; `reason` explains why.
2742
+ * - `device_type === 'desktop'` → render `qr_payload` as a QR; the
2743
+ * mobile device picks up the session via the connect endpoint.
2744
+ * - `device_type === 'mobile'` → open the face capture modal directly.
2659
2745
  *
2660
- * @param request - Request containing the reference, customer_id, optional redirect URL, and callback URL (receives POST requests)
2746
+ * @param request - Reference, customer_id, event, amount (for threshold events), optional URLs.
2661
2747
  */
2662
2748
  async createReuseKycSession(request) {
2663
- return this.requestWithRetry("/api/v1/kyc/face/session", {
2749
+ return this.request("/api/v1/kyc/face/session", {
2664
2750
  method: "POST",
2665
2751
  body: JSON.stringify(request),
2666
2752
  headers: this.getUserHeaders()
2667
2753
  });
2668
2754
  }
2669
2755
  /**
2670
- * Submit a reuse KYC session for validate a user with existing KYC verification
2756
+ * Submit a real-time face capture for an active Re-Use KYC session.
2757
+ *
2758
+ * **Mobile-only.** The server rejects desktop User-Agents with HTTP
2759
+ * 400 (`face capture must be completed on a mobile device`). Use the
2760
+ * QR handoff from `createReuseKycSession` for desktop callers.
2761
+ *
2762
+ * The `data` field on the response carries `retries_remaining`,
2763
+ * `retry_limit_exceeded`, and the reaction flags (`enforce_logout`,
2764
+ * `freeze_account`, etc.) so the tenant app can act on a final failure.
2671
2765
  *
2672
- * @param request - Request containing the reference, token and proof (receives POST requests)
2766
+ * @param request - Token from `createReuseKycSession`, plus the base64 selfie.
2673
2767
  */
2674
2768
  async submitReuseKycSession(request) {
2675
- return this.requestWithRetry("/api/v1/kyc/face/submit", {
2769
+ return this.request("/api/v1/kyc/face/submit", {
2676
2770
  method: "POST",
2677
2771
  body: JSON.stringify(request),
2678
2772
  headers: this.getUserHeaders()
2679
2773
  });
2680
2774
  }
2681
2775
  /**
2682
- * Check reuse KYC session status for a reference
2683
- * @param reference - The unique reference used for the reuse KYC session (e.g., customer ID or transaction ID)
2684
- * @returns Response with kyc_status and message (reason)
2685
- * **/
2776
+ * Look up the current state of a Re-Use KYC session by its forward
2777
+ * reference. Useful for desktop pollers waiting on the mobile handoff.
2778
+ *
2779
+ * @param reference - The reference used when the session was created.
2780
+ */
2686
2781
  async getReuseKycSessionStatus(reference) {
2687
2782
  return this.requestWithRetry(`/api/v1/kyc/face/verify/${encodeURIComponent(reference)}`, {
2688
2783
  method: "GET",
2689
2784
  headers: this.getUserHeaders()
2690
2785
  });
2691
2786
  }
2787
+ /**
2788
+ * Fetch the Redis-backed handoff session for a token. Same backing
2789
+ * store as normal KYC (`kyc:session:<token>`, 15-minute TTL). Desktop
2790
+ * callers poll `mobile_connected` to detect when a mobile device has
2791
+ * scanned the QR and attached.
2792
+ *
2793
+ * @param token - The session token returned by `createReuseKycSession`.
2794
+ */
2795
+ async getHandoffSession(token) {
2796
+ return this.request(
2797
+ `/api/v1/kyc/session${this.buildQueryString({ token })}`,
2798
+ { headers: this.getUserHeaders() }
2799
+ );
2800
+ }
2801
+ /**
2802
+ * Attach a mobile device to a desktop-initiated session. The mobile
2803
+ * client calls this after scanning the QR code. The desktop poller
2804
+ * sees `mobile_connected: true` on the next `getHandoffSession`.
2805
+ *
2806
+ * @param token - The session token transferred via the QR payload.
2807
+ * @param isDisconnect - Pass true to release the session (default false).
2808
+ */
2809
+ async connectMobileSession(token, isDisconnect = false) {
2810
+ return this.request("/api/v1/kyc/session/connect", {
2811
+ method: "PUT",
2812
+ body: JSON.stringify({ token, is_disconnect: isDisconnect }),
2813
+ headers: this.getUserHeaders()
2814
+ });
2815
+ }
2692
2816
  /**
2693
2817
  * Check KYC status for a user
2694
2818
  *
@@ -3047,8 +3171,9 @@ var KycClient = class extends BaseClient {
3047
3171
  */
3048
3172
  async riskProfileRequest(path, options = {}) {
3049
3173
  if (!this.riskProfileBaseURL) {
3050
- throw new Error(
3051
- "Risk Profile Service URL not configured. Please provide riskProfileBaseURL in KycClientConfig."
3174
+ throw new ValidationError(
3175
+ "Risk Profile Service URL not configured. Please provide riskProfileBaseURL in KycClientConfig.",
3176
+ ["riskProfileBaseURL"]
3052
3177
  );
3053
3178
  }
3054
3179
  return this.request(path, {
@@ -3160,6 +3285,19 @@ var KycClient = class extends BaseClient {
3160
3285
  // ============================================================================
3161
3286
  };
3162
3287
 
3288
+ // src/kyc/types.ts
3289
+ var KYC_DECLINED_DESCRIPTIONS = {
3290
+ KYC_DOCUMENT_EXPIRED: "The submitted document has expired",
3291
+ KYC_DOCUMENT_INVALID: "The submitted document could not be verified",
3292
+ KYC_FACE_MISMATCH: "Face verification did not match the identity document",
3293
+ KYC_AGE_REQUIREMENT: "Age requirement not met",
3294
+ KYC_DUPLICATE_IDENTITY: "This identity has already been verified under another account",
3295
+ KYC_ADDRESS_MISMATCH: "Address verification failed",
3296
+ KYC_NAME_MISMATCH: "Name on document does not match the registered name",
3297
+ KYC_PROVIDER_REJECTED: "Identity verification was rejected by the verification provider",
3298
+ KYC_DECLINED: "Identity verification was declined"
3299
+ };
3300
+
3163
3301
  // src/tax/client.ts
3164
3302
  var TaxClient = class extends BaseClient {
3165
3303
  constructor(config) {
@@ -3226,41 +3364,6 @@ var TaxClient = class extends BaseClient {
3226
3364
  { ...requestOptions, responseType: "arraybuffer" }
3227
3365
  );
3228
3366
  }
3229
- async runReminders() {
3230
- return this.request("/api/v1/tax/reminders/run", {
3231
- method: "POST"
3232
- });
3233
- }
3234
- /**
3235
- * Update only the reminder configuration (frequency + max count) without
3236
- * touching any other tax rule settings. Fetches current rules first and
3237
- * merges the reminder fields before sending the update.
3238
- *
3239
- * Requires the caller to be authenticated as a Tenant Super Admin.
3240
- * Throws VesantError with status 403 if the role requirement is not met.
3241
- */
3242
- async updateReminderConfig(input) {
3243
- const current = await this.getTaxRules();
3244
- return this.updateTaxRules({
3245
- trigger_account_creation: current.trigger_account_creation,
3246
- trigger_first_withdrawal: current.trigger_first_withdrawal,
3247
- trigger_threshold: current.trigger_threshold,
3248
- trigger_manual: current.trigger_manual,
3249
- trigger_tin_invalid: current.trigger_tin_invalid,
3250
- trigger_w8ben_expiry: current.trigger_w8ben_expiry,
3251
- trigger_tin_expired: current.trigger_tin_expired,
3252
- us_withholding_enabled: current.us_withholding_enabled,
3253
- non_us_withholding_enabled: current.non_us_withholding_enabled,
3254
- transaction_action: current.transaction_action,
3255
- w8ben_expiry_warning_days: current.w8ben_expiry_warning_days,
3256
- tin_verification_due_date: current.tin_verification_due_date,
3257
- email_sending_method: current.email_sending_method,
3258
- display_tin_links_on_platform: current.display_tin_links_on_platform,
3259
- tax_treaty_support: current.tax_treaty_support,
3260
- reminder_frequency_days: input.reminder_frequency_days,
3261
- reminder_max_count: input.reminder_max_count
3262
- });
3263
- }
3264
3367
  // ==========================================================================
3265
3368
  // P2 — Tenant Tax Rules
3266
3369
  // ==========================================================================
@@ -3304,136 +3407,15 @@ var TaxClient = class extends BaseClient {
3304
3407
  }
3305
3408
  };
3306
3409
 
3307
- // src/transaction/client.ts
3308
- var TransactionClient = class extends BaseClient {
3309
- constructor(config) {
3310
- super(config);
3311
- }
3312
- // ==========================================================================
3313
- // Transaction creation
3314
- // ==========================================================================
3315
- /**
3316
- * Submit a new transaction record to the monitoring service.
3317
- *
3318
- * The gateway endpoint is `POST /api/v1/tm/transactions` and returns 201
3319
- * with no body on success. The SDK method resolves to `void` to reflect
3320
- * that behaviour.
3321
- *
3322
- * @param request - Data required to create the transaction
3323
- * @param requestOptions - Optional request options (e.g. AbortSignal)
3324
- *
3325
- * @example
3326
- * ```ts
3327
- * const client = new TransactionClient({
3328
- * baseURL: process.env.API_GATEWAY_URL!,
3329
- * tenantId: 'tenant-123',
3330
- * apiKey: 'pk_test_...',
3331
- * });
3332
- *
3333
- * await client.createTransaction({
3334
- * tx_id: 'tx-1',
3335
- * reference: 'ref-1',
3336
- * tenant_id: 'tenant-123',
3337
- * customer_id: 'cust-1',
3338
- * transaction_type: 'deposit',
3339
- * transaction_mode: 'ach',
3340
- * amount: '100.00',
3341
- * currency: 'USD',
3342
- * status: 'pending',
3343
- * source_account: 'acct-1',
3344
- * destination_account: 'acct-2',
3345
- * country: 'US',
3346
- * ip_address: '10.0.0.1',
3347
- * metadata: {},
3348
- * benificiary_comment: '',
3349
- * transaction_date: new Date().toISOString(),
3350
- * });
3351
- * ```
3352
- */
3353
- async createTransaction(request, requestOptions) {
3354
- const data = await this.requestWithRetry("/api/v1/tm/transactions", {
3355
- method: "POST",
3356
- body: JSON.stringify(request)
3357
- }, void 0, void 0, requestOptions);
3358
- return data;
3359
- }
3360
- /**
3361
- *
3362
- * @param transactionId tx_id of the transaction
3363
- * @param requestOptions optional request options (e.g. AbortSignal)
3364
- */
3365
- async getTransaction(transactionId, requestOptions) {
3366
- await this.requestWithRetry(`/api/v1/tm/transactions/status/${transactionId}`, {
3367
- method: "GET"
3368
- }, void 0, void 0, requestOptions);
3369
- }
3370
- };
3371
-
3372
- // src/fraud/client.ts
3373
- var FraudClient = class extends BaseClient {
3374
- /**
3375
- * Submit a single fraud event for scoring.
3376
- */
3377
- async scoreEvent(request, requestOptions) {
3378
- this.validateScoreRequest(request);
3379
- return this.requestWithRetry(
3380
- "/api/v1/fraud/score",
3381
- {
3382
- method: "POST",
3383
- body: JSON.stringify(request)
3384
- },
3385
- void 0,
3386
- void 0,
3387
- requestOptions
3388
- );
3389
- }
3390
- /**
3391
- * Submit multiple fraud events for scoring.
3392
- */
3393
- async scoreEventsBulk(requests, requestOptions) {
3394
- if (!Array.isArray(requests) || requests.length === 0) {
3395
- throw new ValidationError(
3396
- "Invalid bulk score request: at least one score request is required",
3397
- ["requests"]
3398
- );
3399
- }
3400
- requests.forEach((request, index) => this.validateScoreRequest(request, index));
3401
- return this.requestWithRetry(
3402
- "/api/v1/fraud/score/bulk",
3403
- {
3404
- method: "POST",
3405
- body: JSON.stringify(requests)
3406
- },
3407
- void 0,
3408
- void 0,
3409
- requestOptions
3410
- );
3411
- }
3412
- validateScoreRequest(request, index) {
3413
- const errors = [];
3414
- const prefix = index === void 0 ? "" : `requests[${index}].`;
3415
- if (!request.customer_id?.trim()) {
3416
- errors.push(`${prefix}customer_id is required`);
3417
- }
3418
- if (!request.sift_user_id?.trim()) {
3419
- errors.push(`${prefix}sift_user_id is required`);
3420
- }
3421
- if (!request.event_type?.trim()) {
3422
- errors.push(`${prefix}event_type is required`);
3423
- }
3424
- if (errors.length > 0) {
3425
- throw new ValidationError(`Invalid fraud score request: ${errors.join(", ")}`, errors);
3426
- }
3427
- }
3428
- };
3429
-
3430
3410
  // src/webhooks/handler.ts
3431
3411
  var WebhookHandler = class {
3432
3412
  constructor(config) {
3433
3413
  this.handlers = /* @__PURE__ */ new Map();
3434
3414
  this.anyHandlers = [];
3415
+ this.seenEventIds = /* @__PURE__ */ new Map();
3435
3416
  this.secret = config.secret;
3436
3417
  this.tolerance = config.tolerance ?? 3e5;
3418
+ this.replayProtection = config.replayProtection ?? true;
3437
3419
  }
3438
3420
  /**
3439
3421
  * Register a handler for a specific event type.
@@ -3457,7 +3439,7 @@ var WebhookHandler = class {
3457
3439
  async verifyAndParse(body, signature) {
3458
3440
  const isValid = await verifyWebhookSignature(body, signature, this.secret);
3459
3441
  if (!isValid) {
3460
- throw new Error("Invalid webhook signature");
3442
+ throw new ValidationError("Invalid webhook signature", ["signature"]);
3461
3443
  }
3462
3444
  return this.parseAndValidate(body);
3463
3445
  }
@@ -3477,16 +3459,34 @@ var WebhookHandler = class {
3477
3459
  parseAndValidate(body) {
3478
3460
  const event = JSON.parse(body);
3479
3461
  if (!event.type || !event.id || !event.timestamp) {
3480
- throw new Error("Invalid webhook event: missing required fields (type, id, timestamp)");
3462
+ throw new ValidationError("Invalid webhook event: missing required fields (type, id, timestamp)", ["type", "id", "timestamp"]);
3481
3463
  }
3482
3464
  if (this.tolerance > 0) {
3483
3465
  const eventTime = new Date(event.timestamp).getTime();
3484
3466
  const now = Date.now();
3485
3467
  if (Math.abs(now - eventTime) > this.tolerance) {
3486
- throw new Error(
3487
- `Webhook event timestamp is outside tolerance window (${this.tolerance}ms)`
3468
+ throw new ValidationError(
3469
+ `Webhook event timestamp is outside tolerance window (${this.tolerance}ms)`,
3470
+ ["timestamp"]
3471
+ );
3472
+ }
3473
+ }
3474
+ if (this.replayProtection) {
3475
+ if (this.seenEventIds.has(event.id)) {
3476
+ throw new ValidationError(
3477
+ `Duplicate webhook event: ${event.id} has already been processed`,
3478
+ ["id"]
3488
3479
  );
3489
3480
  }
3481
+ const now = Date.now();
3482
+ this.seenEventIds.set(event.id, now);
3483
+ if (this.seenEventIds.size > 1e3) {
3484
+ for (const [id, seenAt] of this.seenEventIds) {
3485
+ if (now - seenAt > this.tolerance) {
3486
+ this.seenEventIds.delete(id);
3487
+ }
3488
+ }
3489
+ }
3490
3490
  }
3491
3491
  return event;
3492
3492
  }
@@ -3519,6 +3519,8 @@ function createWebhookMiddleware(options) {
3519
3519
  res.status(401).json({ error: message });
3520
3520
  } else if (message.includes("tolerance") || message.includes("timestamp")) {
3521
3521
  res.status(400).json({ error: message });
3522
+ } else if (message.includes("Duplicate")) {
3523
+ res.status(409).json({ error: message });
3522
3524
  } else if (next) {
3523
3525
  next(error);
3524
3526
  } else {
@@ -3547,7 +3549,7 @@ function createNextWebhookHandler(options) {
3547
3549
  });
3548
3550
  } catch (error) {
3549
3551
  const message = error instanceof Error ? error.message : "Webhook processing failed";
3550
- const status = message.includes("signature") ? 401 : message.includes("tolerance") || message.includes("timestamp") ? 400 : 500;
3552
+ const status = message.includes("signature") ? 401 : message.includes("tolerance") || message.includes("timestamp") ? 400 : message.includes("Duplicate") ? 409 : 500;
3551
3553
  return new Response(JSON.stringify({ error: message }), {
3552
3554
  status,
3553
3555
  headers: { "Content-Type": "application/json" }
@@ -3579,8 +3581,8 @@ exports.ComplianceBlockedError = ComplianceBlockedError;
3579
3581
  exports.ComplianceClient = ComplianceClient;
3580
3582
  exports.ComplianceError = ComplianceError;
3581
3583
  exports.DEFAULT_CURRENCY_RATES = DEFAULT_CURRENCY_RATES;
3582
- exports.FraudClient = FraudClient;
3583
3584
  exports.GeolocationClient = GeolocationClient;
3585
+ exports.KYC_DECLINED_DESCRIPTIONS = KYC_DECLINED_DESCRIPTIONS;
3584
3586
  exports.KycClient = KycClient;
3585
3587
  exports.NetworkError = NetworkError;
3586
3588
  exports.RateLimitError = RateLimitError;
@@ -3590,7 +3592,6 @@ exports.SDK_VERSION = SDK_VERSION;
3590
3592
  exports.ServiceUnavailableError = ServiceUnavailableError;
3591
3593
  exports.TaxClient = TaxClient;
3592
3594
  exports.TimeoutError = TimeoutError;
3593
- exports.TransactionClient = TransactionClient;
3594
3595
  exports.ValidationError = ValidationError;
3595
3596
  exports.VesantError = VesantError;
3596
3597
  exports.WebhookHandler = WebhookHandler;
@@ -3601,6 +3602,7 @@ exports.decodeCipherText = decodeCipherText;
3601
3602
  exports.generateCipherText = generateCipherText;
3602
3603
  exports.isCipherTextExpired = isCipherTextExpired;
3603
3604
  exports.noopLogger = noopLogger;
3605
+ exports.sdkReasons = sdkReasons;
3604
3606
  exports.verifyWebhookSignature = verifyWebhookSignature;
3605
3607
  //# sourceMappingURL=index.js.map
3606
3608
  //# sourceMappingURL=index.js.map