vesant-sdk 1.7.0 → 1.7.1-dev.0520b46

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 (75) hide show
  1. package/dist/{client-B0qhE2kr.d.mts → client-5q4whhGu.d.mts} +2 -2
  2. package/dist/{client-DF7hlMEz.d.ts → client-BaUekQT8.d.ts} +96 -52
  3. package/dist/{client-DtH2RLuy.d.ts → client-DUHC-68_.d.ts} +2 -2
  4. package/dist/{client-DrjgZoH_.d.mts → client-XDEhh9VN.d.mts} +96 -52
  5. package/dist/{client-BolQlL5e.d.mts → client-q9fN8Hhf.d.mts} +103 -1
  6. package/dist/{client-BolQlL5e.d.ts → client-q9fN8Hhf.d.ts} +103 -1
  7. package/dist/compliance/index.d.mts +3 -3
  8. package/dist/compliance/index.d.ts +3 -3
  9. package/dist/compliance/index.js +218 -90
  10. package/dist/compliance/index.js.map +1 -1
  11. package/dist/compliance/index.mjs +218 -90
  12. package/dist/compliance/index.mjs.map +1 -1
  13. package/dist/decisions/index.d.mts +1 -1
  14. package/dist/decisions/index.d.ts +1 -1
  15. package/dist/decisions/index.js +69 -8
  16. package/dist/decisions/index.js.map +1 -1
  17. package/dist/decisions/index.mjs +69 -8
  18. package/dist/decisions/index.mjs.map +1 -1
  19. package/dist/geolocation/index.d.mts +68 -7
  20. package/dist/geolocation/index.d.ts +68 -7
  21. package/dist/geolocation/index.js +223 -90
  22. package/dist/geolocation/index.js.map +1 -1
  23. package/dist/geolocation/index.mjs +220 -91
  24. package/dist/geolocation/index.mjs.map +1 -1
  25. package/dist/index-C3zUKydT.d.mts +264 -0
  26. package/dist/index-DSDgS09k.d.ts +264 -0
  27. package/dist/index.d.mts +27 -10
  28. package/dist/index.d.ts +27 -10
  29. package/dist/index.js +418 -126
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +409 -127
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/kyc/core.d.mts +2 -2
  34. package/dist/kyc/core.d.ts +2 -2
  35. package/dist/kyc/core.js +102 -21
  36. package/dist/kyc/core.js.map +1 -1
  37. package/dist/kyc/core.mjs +102 -22
  38. package/dist/kyc/core.mjs.map +1 -1
  39. package/dist/kyc/index.d.mts +154 -30
  40. package/dist/kyc/index.d.ts +154 -30
  41. package/dist/kyc/index.js +102 -21
  42. package/dist/kyc/index.js.map +1 -1
  43. package/dist/kyc/index.mjs +102 -22
  44. package/dist/kyc/index.mjs.map +1 -1
  45. package/dist/react.d.mts +14 -3
  46. package/dist/react.d.ts +14 -3
  47. package/dist/react.js +317 -68
  48. package/dist/react.js.map +1 -1
  49. package/dist/react.mjs +317 -68
  50. package/dist/react.mjs.map +1 -1
  51. package/dist/risk-profile/index.d.mts +1 -1
  52. package/dist/risk-profile/index.d.ts +1 -1
  53. package/dist/risk-profile/index.js +69 -8
  54. package/dist/risk-profile/index.js.map +1 -1
  55. package/dist/risk-profile/index.mjs +69 -8
  56. package/dist/risk-profile/index.mjs.map +1 -1
  57. package/dist/scores/index.d.mts +1 -1
  58. package/dist/scores/index.d.ts +1 -1
  59. package/dist/scores/index.js +69 -8
  60. package/dist/scores/index.js.map +1 -1
  61. package/dist/scores/index.mjs +69 -8
  62. package/dist/scores/index.mjs.map +1 -1
  63. package/dist/tax/index.d.mts +2 -2
  64. package/dist/tax/index.d.ts +2 -2
  65. package/dist/tax/index.js +74 -10
  66. package/dist/tax/index.js.map +1 -1
  67. package/dist/tax/index.mjs +74 -10
  68. package/dist/tax/index.mjs.map +1 -1
  69. package/dist/webhooks/index.d.mts +2 -189
  70. package/dist/webhooks/index.d.ts +2 -189
  71. package/dist/webhooks/index.js +85 -21
  72. package/dist/webhooks/index.js.map +1 -1
  73. package/dist/webhooks/index.mjs +85 -22
  74. package/dist/webhooks/index.mjs.map +1 -1
  75. package/package.json +4 -1
package/dist/index.mjs CHANGED
@@ -1,3 +1,59 @@
1
+ // src/core/auth.ts
2
+ var StaticApiKeyProvider = class {
3
+ constructor(apiKey) {
4
+ this.apiKey = apiKey;
5
+ }
6
+ async getCredential() {
7
+ return this.apiKey ? { scheme: "Bearer", token: this.apiKey } : null;
8
+ }
9
+ canRefresh() {
10
+ return false;
11
+ }
12
+ };
13
+ var RefreshingTokenProvider = class {
14
+ constructor(opts) {
15
+ this.opts = opts;
16
+ this.cached = null;
17
+ this.inFlight = null;
18
+ this.skewMs = opts.skewMs ?? 3e4;
19
+ this.scheme = opts.scheme ?? "Bearer";
20
+ this.now = opts.now ?? (() => Date.now());
21
+ }
22
+ canRefresh() {
23
+ return true;
24
+ }
25
+ async getCredential(ctx) {
26
+ const forceRefresh = ctx?.forceRefresh ?? false;
27
+ if (!forceRefresh && this.cached && !this.isExpiring(this.cached)) {
28
+ return { scheme: this.scheme, token: this.cached.token };
29
+ }
30
+ const fresh = await this.refresh();
31
+ return { scheme: this.scheme, token: fresh.token };
32
+ }
33
+ isExpiring(token) {
34
+ if (token.expiresAt === void 0) {
35
+ return false;
36
+ }
37
+ return this.now() >= token.expiresAt - this.skewMs;
38
+ }
39
+ refresh() {
40
+ if (this.inFlight) {
41
+ return this.inFlight;
42
+ }
43
+ this.inFlight = (async () => {
44
+ try {
45
+ const fresh = await this.opts.fetchToken();
46
+ this.cached = fresh;
47
+ this.opts.onRotate?.({ expiresAt: fresh.expiresAt });
48
+ return fresh;
49
+ } finally {
50
+ this.inFlight = null;
51
+ }
52
+ })();
53
+ return this.inFlight;
54
+ }
55
+ };
56
+
1
57
  // src/core/errors.ts
2
58
  var VesantError = class _VesantError extends Error {
3
59
  constructor(message, code, statusCode, details) {
@@ -240,7 +296,7 @@ var noopLogger = {
240
296
  };
241
297
 
242
298
  // src/core/version.ts
243
- var SDK_VERSION = "1.7.0";
299
+ var SDK_VERSION = "1.7.1";
244
300
 
245
301
  // src/shared/browser-utils.ts
246
302
  function generateUUID() {
@@ -322,6 +378,12 @@ var BaseClient = class {
322
378
  if (!config.tenantId?.trim()) {
323
379
  throw new ValidationError("tenantId is required and must be a non-empty string", ["tenantId"]);
324
380
  }
381
+ if (typeof fetch === "undefined") {
382
+ throw new VesantError(
383
+ "The Vesant SDK requires a global fetch (Node.js >= 18). Upgrade Node or provide a fetch polyfill.",
384
+ "RUNTIME_UNSUPPORTED"
385
+ );
386
+ }
325
387
  this.interceptors = config.interceptors || [];
326
388
  this.logger = config.logger || createConsoleLogger();
327
389
  let environment = config.environment;
@@ -340,11 +402,12 @@ var BaseClient = class {
340
402
  environment,
341
403
  headers: config.headers || {},
342
404
  timeout: config.timeout || 1e4,
343
- retries: config.retries || 3,
405
+ retries: config.retries ?? 3,
344
406
  debug: config.debug || false,
345
407
  interceptors: this.interceptors,
346
408
  logger: this.logger
347
409
  };
410
+ this.authProvider = config.authProvider ?? (apiKey ? new StaticApiKeyProvider(apiKey) : void 0);
348
411
  if (config.circuitBreaker) {
349
412
  this.circuitBreaker = new CircuitBreaker(config.circuitBreaker);
350
413
  }
@@ -352,6 +415,19 @@ var BaseClient = class {
352
415
  this.rateLimitTracker = new RateLimitTracker();
353
416
  }
354
417
  }
418
+ /**
419
+ * Resolve the Idempotency-Key for a request. Mutating methods (POST/PUT/PATCH)
420
+ * get the caller-supplied key or a freshly minted UUID; non-mutating methods get
421
+ * none. requestWithRetry resolves this once and injects it so every retry reuses
422
+ * one key (letting the server dedup replays); a direct request() resolves per call.
423
+ */
424
+ resolveIdempotencyKey(method, requestOptions) {
425
+ const isMutating = ["POST", "PUT", "PATCH"].includes((method || "GET").toUpperCase());
426
+ if (!isMutating) {
427
+ return void 0;
428
+ }
429
+ return requestOptions?.idempotencyKey ?? generateUUID();
430
+ }
355
431
  /**
356
432
  * Make an HTTP request with timeout and error handling
357
433
  */
@@ -377,15 +453,20 @@ var BaseClient = class {
377
453
  ...this.config.headers,
378
454
  ...options.headers || {}
379
455
  };
380
- if (this.config.apiKey) {
381
- headers["Authorization"] = `Bearer ${this.config.apiKey}`;
456
+ const credential = await this.authProvider?.getCredential({
457
+ tenantId: this.config.tenantId,
458
+ requestId
459
+ });
460
+ if (credential) {
461
+ headers["Authorization"] = `${credential.scheme ?? "Bearer"} ${credential.token}`;
382
462
  }
383
463
  if (this.config.environment === "sandbox") {
384
464
  headers["X-Sandbox"] = "true";
385
465
  }
386
466
  const method = (options.method || "GET").toUpperCase();
387
- if (["POST", "PUT", "PATCH"].includes(method)) {
388
- headers["Idempotency-Key"] = requestOptions?.idempotencyKey || generateUUID();
467
+ const idempotencyKey = this.resolveIdempotencyKey(method, requestOptions);
468
+ if (idempotencyKey) {
469
+ headers["Idempotency-Key"] = idempotencyKey;
389
470
  }
390
471
  const controller = new AbortController();
391
472
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
@@ -406,10 +487,28 @@ var BaseClient = class {
406
487
  if (this.config.debug) {
407
488
  this.logger.debug(`${finalOptions.method || "GET"} ${endpoint}`);
408
489
  }
409
- const response = await fetch(url, {
490
+ let response = await fetch(url, {
410
491
  ...finalOptions,
411
492
  signal: controller.signal
412
493
  });
494
+ if (response.status === 401 && this.authProvider?.canRefresh?.()) {
495
+ const refreshed = await this.authProvider.getCredential({
496
+ tenantId: this.config.tenantId,
497
+ requestId,
498
+ forceRefresh: true
499
+ });
500
+ if (refreshed) {
501
+ this.config.onCredentialRotated?.({ reason: "unauthorized" });
502
+ finalOptions = {
503
+ ...finalOptions,
504
+ headers: {
505
+ ...finalOptions.headers,
506
+ Authorization: `${refreshed.scheme ?? "Bearer"} ${refreshed.token}`
507
+ }
508
+ };
509
+ response = await fetch(url, { ...finalOptions, signal: controller.signal });
510
+ }
511
+ }
413
512
  clearTimeout(timeoutId);
414
513
  if (this.rateLimitTracker) {
415
514
  this.rateLimitTracker.updateFromHeaders(response.headers);
@@ -498,9 +597,11 @@ var BaseClient = class {
498
597
  */
499
598
  async requestWithRetry(endpoint, options = {}, serviceURL, retries = this.config.retries, requestOptions) {
500
599
  let lastError;
600
+ const idempotencyKey = this.resolveIdempotencyKey(options.method || "GET", requestOptions);
601
+ const attemptOptions = idempotencyKey ? { ...requestOptions, idempotencyKey } : requestOptions;
501
602
  for (let attempt = 0; attempt <= retries; attempt++) {
502
603
  try {
503
- return await this.request(endpoint, options, serviceURL, requestOptions);
604
+ return await this.request(endpoint, options, serviceURL, attemptOptions);
504
605
  } catch (error) {
505
606
  lastError = error instanceof Error ? error : new Error("Unknown error");
506
607
  if (requestOptions?.signal?.aborted) {
@@ -597,6 +698,9 @@ var BaseClient = class {
597
698
  if (config.interceptors) {
598
699
  this.interceptors = config.interceptors;
599
700
  }
701
+ if (config.authProvider !== void 0 || config.apiKey !== void 0) {
702
+ this.authProvider = this.config.authProvider ?? (this.config.apiKey ? new StaticApiKeyProvider(this.config.apiKey) : void 0);
703
+ }
600
704
  }
601
705
  /**
602
706
  * Get current configuration (readonly)
@@ -624,8 +728,69 @@ var BaseClient = class {
624
728
  }
625
729
  };
626
730
 
731
+ // src/core/pagination.ts
732
+ async function* paginate(fetchPage) {
733
+ let page = 1;
734
+ let totalPages;
735
+ do {
736
+ const response = await fetchPage(page);
737
+ const items = response.data ?? [];
738
+ for (const item of items) {
739
+ yield item;
740
+ }
741
+ totalPages = response.total_pages ?? page;
742
+ if (items.length === 0) {
743
+ break;
744
+ }
745
+ page += 1;
746
+ } while (page <= totalPages);
747
+ }
748
+ async function collectAll(fetchPage) {
749
+ const items = [];
750
+ for await (const item of paginate(fetchPage)) {
751
+ items.push(item);
752
+ }
753
+ return items;
754
+ }
755
+
756
+ // src/core/dedup.ts
757
+ var InMemoryDedupStore = class {
758
+ constructor(opts = {}) {
759
+ this.expiryById = /* @__PURE__ */ new Map();
760
+ this.maxEntries = opts.maxEntries ?? 1e3;
761
+ this.now = opts.now ?? (() => Date.now());
762
+ }
763
+ async seen(id) {
764
+ return this.expiryById.has(id);
765
+ }
766
+ async mark(id, ttlMs) {
767
+ const now = this.now();
768
+ this.expiryById.set(id, now + ttlMs);
769
+ if (this.expiryById.size > this.maxEntries) {
770
+ for (const [key, expiresAt] of this.expiryById) {
771
+ if (now > expiresAt) {
772
+ this.expiryById.delete(key);
773
+ }
774
+ }
775
+ }
776
+ }
777
+ };
778
+
627
779
  // src/core/webhook-utils.ts
628
- async function verifyWebhookSignature(payload, signature, secret) {
780
+ async function verifyWebhookSignature(payload, signature, secret, opts = {}) {
781
+ const signed = parseSignedTimestamp(signature);
782
+ if (signed) {
783
+ const tolerance = opts.tolerance ?? 3e5;
784
+ const now = opts.now?.() ?? Date.now();
785
+ if (tolerance > 0 && Math.abs(now - signed.t * 1e3) > tolerance) {
786
+ return false;
787
+ }
788
+ const expected = await computeHmacSha256(`${signed.t}.${payload}`, secret);
789
+ return constantTimeEqual(signed.v1, expected);
790
+ }
791
+ if (opts.allowLegacy === false) {
792
+ return false;
793
+ }
629
794
  const hexDigest = await computeHmacSha256(payload, secret);
630
795
  const expectedPrefixed = `sha256=${hexDigest}`;
631
796
  if (signature.startsWith("sha256=")) {
@@ -633,6 +798,25 @@ async function verifyWebhookSignature(payload, signature, secret) {
633
798
  }
634
799
  return constantTimeEqual(signature, hexDigest);
635
800
  }
801
+ function parseSignedTimestamp(signature) {
802
+ if (!signature.includes("t=") || !signature.includes("v1=")) {
803
+ return null;
804
+ }
805
+ let t;
806
+ let v1;
807
+ for (const part of signature.split(",")) {
808
+ const eq = part.indexOf("=");
809
+ if (eq === -1) continue;
810
+ const key = part.slice(0, eq).trim();
811
+ const value = part.slice(eq + 1).trim();
812
+ if (key === "t") t = Number(value);
813
+ else if (key === "v1") v1 = value;
814
+ }
815
+ if (t === void 0 || Number.isNaN(t) || !v1) {
816
+ return null;
817
+ }
818
+ return { t, v1 };
819
+ }
636
820
  async function computeHmacSha256(message, secret) {
637
821
  if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.subtle) {
638
822
  const encoder = new TextEncoder();
@@ -667,27 +851,63 @@ function constantTimeEqual(a, b) {
667
851
  return result === 0;
668
852
  }
669
853
 
670
- // src/geolocation/ciphertext.ts
671
- var CIPHER_TEXT_EXPIRY_MINUTES = 5;
672
- async function computeHMAC(key, message) {
673
- if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.subtle) {
674
- const encoder = new TextEncoder();
675
- const keyData = encoder.encode(key);
676
- const msgData = encoder.encode(message);
677
- const cryptoKey = await globalThis.crypto.subtle.importKey(
678
- "raw",
679
- keyData,
680
- { name: "HMAC", hash: "SHA-256" },
681
- false,
682
- ["sign"]
683
- );
684
- const signature = await globalThis.crypto.subtle.sign("HMAC", cryptoKey, msgData);
685
- const bytes = new Uint8Array(signature);
686
- return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
854
+ // src/geolocation/integrity.ts
855
+ var isNil = (v) => v === null || v === void 0;
856
+ function assessGpsIntegrity(input, opts = {}) {
857
+ const flags = [];
858
+ const now = opts.now ?? (() => Date.now());
859
+ const maxAgeMs = opts.maxAgeMs ?? 6e4;
860
+ const minAccuracy = opts.minAccuracyMeters ?? 1;
861
+ const { accuracy, altitude, altitudeAccuracy, heading, speed, timestamp } = input;
862
+ if (isNil(accuracy)) {
863
+ flags.push("accuracy_missing");
864
+ } else if (accuracy === 0) {
865
+ flags.push("accuracy_zero");
866
+ } else if (accuracy < minAccuracy) {
867
+ flags.push("accuracy_implausibly_small");
868
+ }
869
+ const hasMotion = !isNil(heading) || !isNil(speed);
870
+ if (hasMotion && (isNil(accuracy) || accuracy === 0)) {
871
+ flags.push("motion_without_accuracy");
872
+ }
873
+ if (!isNil(altitude) && isNil(altitudeAccuracy)) {
874
+ flags.push("altitude_without_accuracy");
875
+ }
876
+ if (timestamp !== void 0 && now() - timestamp > maxAgeMs) {
877
+ flags.push("stale_timestamp");
878
+ }
879
+ if (opts.permissionState === "denied" || opts.permissionState === "prompt") {
880
+ flags.push(`permission_${opts.permissionState}`);
881
+ }
882
+ return { suspicious: flags.length > 0, flags };
883
+ }
884
+ function gpsIntegrityInputFromPosition(position) {
885
+ const c = position.coords;
886
+ return {
887
+ latitude: c.latitude,
888
+ longitude: c.longitude,
889
+ accuracy: c.accuracy,
890
+ altitude: c.altitude,
891
+ altitudeAccuracy: c.altitudeAccuracy,
892
+ heading: c.heading,
893
+ speed: c.speed,
894
+ timestamp: position.timestamp
895
+ };
896
+ }
897
+ async function probeGeolocationPermission() {
898
+ try {
899
+ if (typeof navigator === "undefined" || !navigator.permissions?.query) {
900
+ return "unavailable";
901
+ }
902
+ const status = await navigator.permissions.query({ name: "geolocation" });
903
+ return status.state;
904
+ } catch {
905
+ return "unavailable";
687
906
  }
688
- const { createHmac } = await import('crypto');
689
- return createHmac("sha256", key).update(message).digest("hex");
690
907
  }
908
+
909
+ // src/geolocation/ciphertext.ts
910
+ var CIPHER_TEXT_EXPIRY_MINUTES = 5;
691
911
  function getWebGLInfo() {
692
912
  if (typeof document === "undefined") return null;
693
913
  try {
@@ -719,6 +939,7 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
719
939
  if (typeof navigator === "undefined" || !navigator.geolocation) {
720
940
  return null;
721
941
  }
942
+ const permissionState = await probeGeolocationPermission();
722
943
  return new Promise((resolve) => {
723
944
  navigator.geolocation.getCurrentPosition(
724
945
  (position) => {
@@ -735,6 +956,7 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
735
956
  resolve(null);
736
957
  return;
737
958
  }
959
+ const { flags } = assessGpsIntegrity(gpsIntegrityInputFromPosition(position), { permissionState });
738
960
  resolve({
739
961
  latitude,
740
962
  longitude,
@@ -743,7 +965,8 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
743
965
  altitude_accuracy: position.coords.altitudeAccuracy ?? void 0,
744
966
  heading: position.coords.heading ?? void 0,
745
967
  speed: position.coords.speed ?? void 0,
746
- timestamp: position.timestamp
968
+ timestamp: position.timestamp,
969
+ integrity_flags: flags.length ? flags : void 0
747
970
  });
748
971
  },
749
972
  () => {
@@ -810,6 +1033,9 @@ async function generateCipherText(options, config) {
810
1033
  );
811
1034
  if (location) {
812
1035
  locationData = location;
1036
+ if (location.integrity_flags?.length) {
1037
+ warnings.push(`GPS integrity flags (advisory): ${location.integrity_flags.join(", ")}`);
1038
+ }
813
1039
  } else if (gpsRequiredByConfig) {
814
1040
  throw new VesantError(
815
1041
  `GPS location is required for ${options.reason} by tenant configuration, but GPS was not available or permission was denied`,
@@ -839,15 +1065,7 @@ async function generateCipherText(options, config) {
839
1065
  };
840
1066
  const encoded = encodePayload(payload);
841
1067
  const timestamp = now.getTime().toString(36);
842
- let cipherText;
843
- const hmacKey = options.signingKey || options.apiKey;
844
- if (hmacKey) {
845
- const message = `02.${timestamp}.${encoded}`;
846
- const hmac = await computeHMAC(hmacKey, message);
847
- cipherText = `${message}.${hmac}`;
848
- } else {
849
- cipherText = `01.${timestamp}.${encoded}`;
850
- }
1068
+ const cipherText = `01.${timestamp}.${encoded}`;
851
1069
  return {
852
1070
  cipherText,
853
1071
  locationCaptured: !!locationData,
@@ -856,6 +1074,7 @@ async function generateCipherText(options, config) {
856
1074
  expiresAt: expiry.toISOString()
857
1075
  };
858
1076
  }
1077
+ var collectDeviceSignals = generateCipherText;
859
1078
  function decodeCipherText(cipherText) {
860
1079
  try {
861
1080
  const parts = cipherText.split(".");
@@ -883,6 +1102,7 @@ function isCipherTextExpired(cipherText) {
883
1102
  }
884
1103
 
885
1104
  // src/geolocation/client.ts
1105
+ var DEFAULT_GPS_CONFIG_TTL_MS = 5 * 60 * 1e3;
886
1106
  var GeolocationClient = class extends BaseClient {
887
1107
  constructor(config) {
888
1108
  const baseConfig = {
@@ -898,6 +1118,8 @@ var GeolocationClient = class extends BaseClient {
898
1118
  environment: config.environment
899
1119
  };
900
1120
  super(baseConfig);
1121
+ this.gpsConfigCache = null;
1122
+ this.gpsConfigTtlMs = config.gpsConfigTtlMs ?? DEFAULT_GPS_CONFIG_TTL_MS;
901
1123
  }
902
1124
  // ============================================================================
903
1125
  // IP Verification & Compliance
@@ -929,8 +1151,8 @@ var GeolocationClient = class extends BaseClient {
929
1151
  * ```
930
1152
  */
931
1153
  async verifyIP(request, requestOptions) {
932
- if (!request.ip_address?.trim()) {
933
- throw new ValidationError("ip_address is required and must be a non-empty string", ["ip_address"]);
1154
+ if (request.ip_address !== void 0 && !request.ip_address.trim()) {
1155
+ throw new ValidationError("ip_address, when provided, must be a non-empty string", ["ip_address"]);
934
1156
  }
935
1157
  return this.requestWithRetry("/api/v1/geo/verify", {
936
1158
  method: "POST",
@@ -983,37 +1205,85 @@ var GeolocationClient = class extends BaseClient {
983
1205
  * ```
984
1206
  */
985
1207
  async getGPSConfig(requestOptions) {
986
- const config = await this.requestWithRetry("/api/v1/geo/config", void 0, void 0, void 0, requestOptions);
987
- if (config.signing_key) {
988
- this.cachedSigningKey = config.signing_key;
1208
+ const cached = this.gpsConfigCache;
1209
+ if (cached && cached.expiresAt > Date.now()) {
1210
+ return cached.value;
989
1211
  }
990
- return config;
1212
+ return this.refreshGPSConfig(requestOptions);
1213
+ }
1214
+ /**
1215
+ * Re-read the tenant's GPS requirement configuration, ignoring any copy held
1216
+ * from an earlier read.
1217
+ *
1218
+ * These requirements are edited in the compliance console while your
1219
+ * application is running. `getGPSConfig` reuses a fetched copy for a short
1220
+ * period so a long-lived session does not call the API on every event; call
1221
+ * this when you need a change to take effect immediately — for example right
1222
+ * after an administrator saves new settings.
1223
+ *
1224
+ * @returns The current GPS requirement config per event type
1225
+ *
1226
+ * @example
1227
+ * ```typescript
1228
+ * const config = await client.refreshGPSConfig();
1229
+ * if (config.require_gps.login) {
1230
+ * // GPS is now required for login
1231
+ * }
1232
+ * ```
1233
+ */
1234
+ async refreshGPSConfig(requestOptions) {
1235
+ const value = await this.requestWithRetry(
1236
+ "/api/v1/geo/config",
1237
+ void 0,
1238
+ void 0,
1239
+ void 0,
1240
+ requestOptions
1241
+ );
1242
+ this.gpsConfigCache = { value, expiresAt: Date.now() + this.gpsConfigTtlMs };
1243
+ return value;
991
1244
  }
992
1245
  /**
993
- * Fetch the signing key from the dedicated authenticated endpoint.
1246
+ * Mint a short-lived, single-use device-signals nonce.
994
1247
  *
995
- * Falls back to getGPSConfig() for backward compatibility with older servers
996
- * that don't expose `/api/v1/geo/signing-key`.
1248
+ * The nonce is bound to the tenant, user, and event type and is consumed
1249
+ * server-side during validation, so a captured device-signals envelope cannot
1250
+ * be replayed. `validateCipherText` mints and attaches one automatically; call
1251
+ * this directly only if you manage the nonce yourself.
997
1252
  *
998
- * @returns The signing key string, or undefined if unavailable
1253
+ * @param userId - User ID the nonce is bound to
1254
+ * @param eventType - Event type the nonce is bound to (login, registration, ...)
1255
+ * @returns The nonce and its expiry
1256
+ */
1257
+ async fetchSignalsNonce(userId, eventType, requestOptions) {
1258
+ if (!userId?.trim()) {
1259
+ throw new ValidationError("userId is required and must be a non-empty string", ["userId"]);
1260
+ }
1261
+ const request = { user_id: userId, event_type: eventType };
1262
+ return this.request(
1263
+ "/api/v1/geo/signals-token",
1264
+ {
1265
+ method: "POST",
1266
+ body: JSON.stringify(request)
1267
+ },
1268
+ void 0,
1269
+ requestOptions
1270
+ );
1271
+ }
1272
+ /**
1273
+ * Best-effort nonce mint for validation. Returns undefined if minting fails —
1274
+ * the server is the sole authority and blocks when a nonce is required, so this
1275
+ * never downgrades security on the client's say-so.
999
1276
  */
1000
- async fetchSigningKey(requestOptions) {
1277
+ async resolveSignalsNonce(userId, eventType, requestOptions) {
1001
1278
  try {
1002
- const response = await this.requestWithRetry(
1003
- "/api/v1/geo/signing-key",
1004
- void 0,
1005
- void 0,
1006
- void 0,
1007
- requestOptions
1279
+ const { nonce } = await this.fetchSignalsNonce(userId, eventType, requestOptions);
1280
+ return nonce;
1281
+ } catch (err) {
1282
+ this.logger.warn(
1283
+ `Failed to mint device-signals nonce; proceeding without it: ${err instanceof Error ? err.message : String(err)}`
1008
1284
  );
1009
- if (response.signing_key) {
1010
- this.cachedSigningKey = response.signing_key;
1011
- return response.signing_key;
1012
- }
1013
- } catch {
1285
+ return void 0;
1014
1286
  }
1015
- const config = await this.getGPSConfig(requestOptions);
1016
- return config.signing_key;
1017
1287
  }
1018
1288
  // ============================================================================
1019
1289
  // CipherText Validation
@@ -1066,12 +1336,14 @@ var GeolocationClient = class extends BaseClient {
1066
1336
  if (!userId?.trim()) {
1067
1337
  throw new ValidationError("userId is required and must be a non-empty string", ["userId"]);
1068
1338
  }
1339
+ const nonce = await this.resolveSignalsNonce(userId, eventType, requestOptions);
1069
1340
  const request = {
1070
1341
  cipher_text: cipherText,
1071
1342
  user_id: userId,
1072
1343
  event_type: eventType,
1073
1344
  expected_ip: expectedIP,
1074
- customer_data: customerData
1345
+ customer_data: customerData,
1346
+ nonce
1075
1347
  };
1076
1348
  return this.requestWithRetry(
1077
1349
  "/api/v1/geo/validate-ciphertext",
@@ -1280,20 +1552,12 @@ var GeolocationClient = class extends BaseClient {
1280
1552
  *
1281
1553
  * @example
1282
1554
  * ```typescript
1283
- * // Using GPS (preferred)
1555
+ * // GPS (the web SDK is GPS + IP only; the server handles the IP fallback)
1284
1556
  * const result = await client.captureLocation(token, {
1285
1557
  * latitude: 37.7749,
1286
1558
  * longitude: -122.4194,
1287
1559
  * accuracy: 10
1288
1560
  * });
1289
- *
1290
- * // Using WiFi positioning (fallback)
1291
- * const result = await client.captureLocation(token, {
1292
- * wifi_networks: [
1293
- * { macAddress: "00:11:22:33:44:55", signalStrength: -50 },
1294
- * { macAddress: "AA:BB:CC:DD:EE:FF", signalStrength: -70 }
1295
- * ]
1296
- * });
1297
1561
  * ```
1298
1562
  */
1299
1563
  async captureLocation(token, capture, requestOptions) {
@@ -1306,34 +1570,22 @@ var GeolocationClient = class extends BaseClient {
1306
1570
  // CipherText Generation
1307
1571
  // ============================================================================
1308
1572
  /**
1309
- * Generate a signed cipherText containing device and location data.
1573
+ * Generate a device-signals envelope containing device and location data,
1574
+ * validated by the server.
1310
1575
  *
1311
- * Automatically passes the client's API key for HMAC signing (v02 format).
1312
- * If no API key is configured, falls back to unsigned v01 format.
1313
- *
1314
- * @param options - Options for cipherText generation
1576
+ * @param options - Options for envelope generation
1315
1577
  * @param gpsConfig - Optional GPS config (from getGPSConfig)
1316
- * @returns CipherText result with the signed string
1578
+ * @returns The device-signals envelope result
1317
1579
  *
1318
1580
  * @example
1319
1581
  * ```typescript
1320
1582
  * const result = await client.generateCipherText({ reason: 'login' });
1321
- * console.log(result.cipherText); // v02 signed cipherText
1583
+ * console.log(result.cipherText); // device-signals envelope
1322
1584
  * ```
1323
1585
  */
1324
1586
  async generateCipherText(options, gpsConfig) {
1325
- let signingKey = this.cachedSigningKey;
1326
- let resolvedGpsConfig = gpsConfig;
1327
- if (!signingKey) {
1328
- signingKey = await this.fetchSigningKey();
1329
- }
1330
- if (!resolvedGpsConfig) {
1331
- resolvedGpsConfig = await this.getGPSConfig();
1332
- }
1333
- return generateCipherText(
1334
- { ...options, signingKey: signingKey || void 0 },
1335
- resolvedGpsConfig
1336
- );
1587
+ const resolvedGpsConfig = gpsConfig ?? await this.getGPSConfig();
1588
+ return generateCipherText(options, resolvedGpsConfig);
1337
1589
  }
1338
1590
  // ============================================================================
1339
1591
  // Utility Methods (inherited from BaseClient: healthCheck, updateConfig, getConfig, buildQueryString)
@@ -2736,21 +2988,20 @@ var KycClient = class extends BaseClient {
2736
2988
  * console.log(`KYC ID: ${result.kyc_id}`);
2737
2989
  * ```
2738
2990
  */
2739
- async requestKycSubmitLink(request) {
2991
+ async requestKycSubmitLink(request, requestOptions) {
2740
2992
  return this.requestWithRetry("/api/v1/kyc/request", {
2741
2993
  method: "POST",
2742
2994
  body: JSON.stringify(request),
2743
2995
  headers: this.getUserHeaders()
2744
- });
2996
+ }, void 0, void 0, requestOptions);
2745
2997
  }
2746
2998
  /**
2747
2999
  * Create a Event-Based Face Verification session.
2748
3000
  *
2749
3001
  * Inspect the response before showing UI:
2750
- * - `is_required === false` → skip face capture; `reason` explains why.
2751
- * - `device_type === 'desktop'` → render `qr_payload` as a QR; the
2752
- * mobile device picks up the session via the connect endpoint.
2753
- * - `device_type === 'mobile'` → open the face capture modal directly.
3002
+ * - `is_required === false` → skip face verification; `reason` explains why.
3003
+ * - `verification_url` present → embed the hosted journey and poll status.
3004
+ * - `verification_url` absent → use the local camera / QR fallback.
2754
3005
  *
2755
3006
  * @param request - Reference, customer_id, event, amount (for threshold events), optional URLs.
2756
3007
  */
@@ -2774,12 +3025,12 @@ var KycClient = class extends BaseClient {
2774
3025
  *
2775
3026
  * @param request - Token from `createEventBasedFaceVerificationSession`, plus the base64 selfie.
2776
3027
  */
2777
- async submitEventBasedFaceVerificationSession(request) {
3028
+ async submitEventBasedFaceVerificationSession(request, requestOptions) {
2778
3029
  return this.request("/api/v1/kyc/face/submit", {
2779
3030
  method: "POST",
2780
3031
  body: JSON.stringify(request),
2781
3032
  headers: this.getUserHeaders()
2782
- });
3033
+ }, void 0, requestOptions);
2783
3034
  }
2784
3035
  /**
2785
3036
  * Look up the current state of a Event-Based Face Verification session by its
@@ -2797,11 +3048,28 @@ var KycClient = class extends BaseClient {
2797
3048
  headers: this.getUserHeaders()
2798
3049
  });
2799
3050
  }
3051
+ /**
3052
+ * Mint a fresh hosted verification journey for the next attempt on an
3053
+ * active session whose previous hosted-journey attempt was declined.
3054
+ * Only valid while the session is active and attempts remain; the
3055
+ * response carries the new `verification_url` to embed. Used internally
3056
+ * by `FaceCaptureModal`'s Try Again flow in hosted-journey mode.
3057
+ *
3058
+ * @param token - The session token returned by `createEventBasedFaceVerificationSession`.
3059
+ */
3060
+ async retryEventBasedFaceVerificationJourney(token) {
3061
+ return this.request("/api/v1/kyc/face/onsite/journey", {
3062
+ method: "POST",
3063
+ body: JSON.stringify({ token }),
3064
+ headers: this.getUserHeaders()
3065
+ });
3066
+ }
2800
3067
  /**
2801
3068
  * Fetch the Redis-backed handoff session for a token. Same backing
2802
- * store as normal KYC (`kyc:session:<token>`, 15-minute TTL). Desktop
2803
- * callers poll `mobile_connected` to detect when a mobile device has
2804
- * scanned the QR and attached.
3069
+ * store as normal KYC (`kyc:session:<token>`, 15-minute TTL). The
3070
+ * Event-Based Face Verification database session still expires after
3071
+ * 10 minutes. Desktop callers poll `mobile_connected` to detect when a
3072
+ * mobile device has scanned the QR and attached.
2805
3073
  *
2806
3074
  * @param token - The session token returned by `createEventBasedFaceVerificationSession`.
2807
3075
  */
@@ -2895,12 +3163,12 @@ var KycClient = class extends BaseClient {
2895
3163
  * console.log(`Verification submitted: ${result.reference}`);
2896
3164
  * ```
2897
3165
  */
2898
- async submitVerification(request) {
3166
+ async submitVerification(request, requestOptions) {
2899
3167
  return this.requestWithRetry("/api/v1/kyc/submit", {
2900
3168
  method: "POST",
2901
3169
  body: JSON.stringify(request),
2902
3170
  headers: this.getUserHeaders()
2903
- });
3171
+ }, void 0, void 0, requestOptions);
2904
3172
  }
2905
3173
  /**
2906
3174
  * Get a KYC request by ID
@@ -3310,6 +3578,9 @@ var KYC_DECLINED_DESCRIPTIONS = {
3310
3578
  KYC_PROVIDER_REJECTED: "Identity verification was rejected by the verification provider",
3311
3579
  KYC_DECLINED: "Identity verification was declined"
3312
3580
  };
3581
+ function isKycDocumentExpiredCallbackEvent(value) {
3582
+ return typeof value === "object" && value !== null && value.event === "kyc_document_expired";
3583
+ }
3313
3584
 
3314
3585
  // src/tax/client.ts
3315
3586
  var TaxClient = class extends BaseClient {
@@ -3340,10 +3611,13 @@ var TaxClient = class extends BaseClient {
3340
3611
  { method: "POST" }
3341
3612
  );
3342
3613
  }
3343
- async checkTINStatus(customerID) {
3614
+ async checkTINStatus(customerID, requestOptions) {
3344
3615
  return this.requestWithRetry(
3345
3616
  `/api/v1/tax/customer-tax-profiles/${customerID}/check-tin`,
3346
- { method: "POST" }
3617
+ { method: "POST" },
3618
+ void 0,
3619
+ void 0,
3620
+ requestOptions
3347
3621
  );
3348
3622
  }
3349
3623
  async reRequestTaxForm(customerID, input) {
@@ -3424,14 +3698,24 @@ var TaxClient = class extends BaseClient {
3424
3698
  };
3425
3699
 
3426
3700
  // src/webhooks/handler.ts
3701
+ function resolveEventId(event) {
3702
+ if (event.id) {
3703
+ return event.id;
3704
+ }
3705
+ if (event.event_type === "notification.created") {
3706
+ return event.notification?.id;
3707
+ }
3708
+ return void 0;
3709
+ }
3427
3710
  var WebhookHandler = class {
3428
3711
  constructor(config) {
3429
3712
  this.handlers = /* @__PURE__ */ new Map();
3430
3713
  this.anyHandlers = [];
3431
- this.seenEventIds = /* @__PURE__ */ new Map();
3432
3714
  this.secret = config.secret;
3433
3715
  this.tolerance = config.tolerance ?? 3e5;
3434
3716
  this.replayProtection = config.replayProtection ?? true;
3717
+ this.allowLegacySignature = config.allowLegacySignature ?? true;
3718
+ this.dedupStore = config.dedupStore ?? new InMemoryDedupStore();
3435
3719
  }
3436
3720
  /**
3437
3721
  * Register a handler for a specific event type.
@@ -3453,7 +3737,10 @@ var WebhookHandler = class {
3453
3737
  * Verify signature and parse the webhook body.
3454
3738
  */
3455
3739
  async verifyAndParse(body, signature) {
3456
- const isValid = await verifyWebhookSignature(body, signature, this.secret);
3740
+ const isValid = await verifyWebhookSignature(body, signature, this.secret, {
3741
+ tolerance: this.tolerance,
3742
+ allowLegacy: this.allowLegacySignature
3743
+ });
3457
3744
  if (!isValid) {
3458
3745
  throw new ValidationError("Invalid webhook signature", ["signature"]);
3459
3746
  }
@@ -3469,18 +3756,21 @@ var WebhookHandler = class {
3469
3756
  /**
3470
3757
  * Parse an event without signature verification (for testing).
3471
3758
  */
3472
- parseEvent(body) {
3759
+ async parseEvent(body) {
3473
3760
  return this.parseAndValidate(body);
3474
3761
  }
3475
- parseAndValidate(body) {
3762
+ async parseAndValidate(body) {
3476
3763
  const event = JSON.parse(body);
3477
- if (!event.type || !event.id || !event.timestamp) {
3478
- throw new ValidationError("Invalid webhook event: missing required fields (type, id, timestamp)", ["type", "id", "timestamp"]);
3764
+ const eventId = resolveEventId(event);
3765
+ if (!event.event_type || !eventId || !event.timestamp) {
3766
+ throw new ValidationError(
3767
+ "Invalid webhook event: missing required fields (event_type, id, timestamp)",
3768
+ ["event_type", "id", "timestamp"]
3769
+ );
3479
3770
  }
3480
3771
  if (this.tolerance > 0) {
3481
3772
  const eventTime = new Date(event.timestamp).getTime();
3482
- const now = Date.now();
3483
- if (Math.abs(now - eventTime) > this.tolerance) {
3773
+ if (Math.abs(Date.now() - eventTime) > this.tolerance) {
3484
3774
  throw new ValidationError(
3485
3775
  `Webhook event timestamp is outside tolerance window (${this.tolerance}ms)`,
3486
3776
  ["timestamp"]
@@ -3488,26 +3778,18 @@ var WebhookHandler = class {
3488
3778
  }
3489
3779
  }
3490
3780
  if (this.replayProtection) {
3491
- if (this.seenEventIds.has(event.id)) {
3781
+ if (await this.dedupStore.seen(eventId)) {
3492
3782
  throw new ValidationError(
3493
- `Duplicate webhook event: ${event.id} has already been processed`,
3783
+ `Duplicate webhook event: ${eventId} has already been processed`,
3494
3784
  ["id"]
3495
3785
  );
3496
3786
  }
3497
- const now = Date.now();
3498
- this.seenEventIds.set(event.id, now);
3499
- if (this.seenEventIds.size > 1e3) {
3500
- for (const [id, seenAt] of this.seenEventIds) {
3501
- if (now - seenAt > this.tolerance) {
3502
- this.seenEventIds.delete(id);
3503
- }
3504
- }
3505
- }
3787
+ await this.dedupStore.mark(eventId, this.tolerance);
3506
3788
  }
3507
3789
  return event;
3508
3790
  }
3509
3791
  async dispatch(event) {
3510
- const typeHandlers = this.handlers.get(event.type) || [];
3792
+ const typeHandlers = this.handlers.get(event.event_type) || [];
3511
3793
  const allHandlers = [...typeHandlers, ...this.anyHandlers];
3512
3794
  for (const handler of allHandlers) {
3513
3795
  await handler(event);
@@ -3588,6 +3870,6 @@ function buildHandler(options) {
3588
3870
  return handler;
3589
3871
  }
3590
3872
 
3591
- 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 };
3873
+ export { AuthenticationError, BaseClient, CGSError, CircuitBreaker, CircuitBreakerOpenError, ComplianceBlockedError, ComplianceClient, ComplianceError, DEFAULT_CURRENCY_RATES, GeolocationClient, InMemoryDedupStore, KYC_DECLINED_DESCRIPTIONS, KycClient, NetworkError, RateLimitError, RateLimitTracker, RefreshingTokenProvider, RiskProfileClient, SDK_VERSION, ServiceUnavailableError, StaticApiKeyProvider, TaxClient, TimeoutError, ValidationError, VesantError, WebhookHandler, assessGpsIntegrity, collectAll, collectDeviceSignals, createConsoleLogger, createNextWebhookHandler, createWebhookMiddleware, decodeCipherText, generateCipherText, gpsIntegrityInputFromPosition, isCipherTextExpired, isKycDocumentExpiredCallbackEvent, noopLogger, paginate, probeGeolocationPermission, sdkReasons, verifyWebhookSignature };
3592
3874
  //# sourceMappingURL=index.mjs.map
3593
3875
  //# sourceMappingURL=index.mjs.map