vesant-sdk 1.6.6 → 1.7.0-dev.9f7da53

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