@ozura/elements 1.1.0 → 1.2.0-next.27

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 (45) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +73 -71
  3. package/dist/frame/element-frame.js +48 -3
  4. package/dist/frame/element-frame.js.map +1 -1
  5. package/dist/frame/tokenizer-frame.html +1 -1
  6. package/dist/frame/tokenizer-frame.js +113 -52
  7. package/dist/frame/tokenizer-frame.js.map +1 -1
  8. package/dist/oz-elements.esm.js +363 -311
  9. package/dist/oz-elements.esm.js.map +1 -1
  10. package/dist/oz-elements.umd.js +364 -311
  11. package/dist/oz-elements.umd.js.map +1 -1
  12. package/dist/react/frame/elementFrame.d.ts +7 -0
  13. package/dist/react/frame/tokenizerFrame.d.ts +4 -1
  14. package/dist/react/index.cjs.js +239 -180
  15. package/dist/react/index.cjs.js.map +1 -1
  16. package/dist/react/index.esm.js +238 -180
  17. package/dist/react/index.esm.js.map +1 -1
  18. package/dist/react/react/index.d.ts +50 -29
  19. package/dist/react/sdk/OzElement.d.ts +3 -1
  20. package/dist/react/sdk/OzVault.d.ts +9 -14
  21. package/dist/react/sdk/createSessionFetcher.d.ts +29 -0
  22. package/dist/react/sdk/index.d.ts +6 -26
  23. package/dist/react/server/index.d.ts +126 -74
  24. package/dist/react/types/index.d.ts +70 -41
  25. package/dist/server/frame/elementFrame.d.ts +7 -0
  26. package/dist/server/frame/tokenizerFrame.d.ts +4 -1
  27. package/dist/server/index.cjs.js +188 -90
  28. package/dist/server/index.cjs.js.map +1 -1
  29. package/dist/server/index.esm.js +187 -91
  30. package/dist/server/index.esm.js.map +1 -1
  31. package/dist/server/sdk/OzElement.d.ts +3 -1
  32. package/dist/server/sdk/OzVault.d.ts +9 -14
  33. package/dist/server/sdk/createSessionFetcher.d.ts +29 -0
  34. package/dist/server/sdk/index.d.ts +6 -26
  35. package/dist/server/server/index.d.ts +126 -74
  36. package/dist/server/types/index.d.ts +70 -41
  37. package/dist/types/frame/elementFrame.d.ts +7 -0
  38. package/dist/types/frame/tokenizerFrame.d.ts +4 -1
  39. package/dist/types/sdk/OzElement.d.ts +3 -1
  40. package/dist/types/sdk/OzVault.d.ts +9 -14
  41. package/dist/types/sdk/createSessionFetcher.d.ts +29 -0
  42. package/dist/types/sdk/index.d.ts +6 -26
  43. package/dist/types/server/index.d.ts +126 -74
  44. package/dist/types/types/index.d.ts +70 -41
  45. package/package.json +1 -1
@@ -324,7 +324,7 @@ function sanitizeOptions(options) {
324
324
  * it never holds raw card data — all sensitive values live in the iframe.
325
325
  */
326
326
  class OzElement {
327
- constructor(elementType, options, vaultId, frameBaseUrl, fonts = [], appearanceStyle) {
327
+ constructor(elementType, options, vaultId, frameBaseUrl, fonts = [], appearanceStyle, onDestroy) {
328
328
  this.iframe = null;
329
329
  this._frameWindow = null;
330
330
  this._ready = false;
@@ -340,6 +340,7 @@ class OzElement {
340
340
  this.fonts = fonts;
341
341
  this.appearanceStyle = appearanceStyle;
342
342
  this.frameId = `oz-${elementType}-${uuid()}`;
343
+ this._onDestroy = onDestroy;
343
344
  }
344
345
  /** The element type this proxy represents. */
345
346
  get type() {
@@ -495,9 +496,14 @@ class OzElement {
495
496
  * and prevents future use. Distinct from `unmount()` which allows re-mounting.
496
497
  */
497
498
  destroy() {
499
+ var _a;
498
500
  this.unmount();
499
501
  this.handlers.clear();
500
502
  this._destroyed = true;
503
+ // Notify OzVault so it can prune the stale frameId entry from its elements
504
+ // and completionState maps. Without this, manually calling el.destroy() leaks
505
+ // map entries that grow unboundedly in SPA scenarios with repeated mount/unmount.
506
+ (_a = this._onDestroy) === null || _a === void 0 ? void 0 : _a.call(this);
501
507
  }
502
508
  // ─── Called by OzVault ───────────────────────────────────────────────────
503
509
  /**
@@ -838,13 +844,116 @@ function validateBilling(billing) {
838
844
  */
839
845
  const PROTOCOL_VERSION = 1;
840
846
 
847
+ /**
848
+ * Creates a `getSessionKey` callback for `OzVault.create()` and `<OzElements>`.
849
+ *
850
+ * This is the recommended way to wire the SDK to your backend session endpoint.
851
+ * If you don't need custom headers or auth logic, pass `sessionUrl` directly to
852
+ * `OzVault.create()` or `<OzElements>` — it calls this helper internally.
853
+ *
854
+ * The callback POSTs `{ sessionId }` to `url` and reads `sessionKey` (or the
855
+ * legacy `waxKey`) from the JSON response, so it is compatible with both the
856
+ * new `createSessionMiddleware` and the old `createMintWaxMiddleware` backends.
857
+ *
858
+ * Each call enforces a **10-second timeout**. On pure network failures
859
+ * (offline, DNS, connection refused) the request is retried **once after 750ms**.
860
+ * HTTP 4xx/5xx errors are never retried — they indicate misconfiguration.
861
+ *
862
+ * @param url - Absolute or relative URL of your session endpoint, e.g. `'/api/oz-session'`.
863
+ *
864
+ * @example
865
+ * // Simplest — just pass sessionUrl, no need to call this directly
866
+ * const vault = await OzVault.create({ pubKey: 'pk_live_...', sessionUrl: '/api/oz-session' });
867
+ *
868
+ * @example
869
+ * // Manual — use when you need custom headers
870
+ * const vault = await OzVault.create({
871
+ * pubKey: 'pk_live_...',
872
+ * getSessionKey: createSessionFetcher('/api/oz-session'),
873
+ * });
874
+ */
875
+ function createSessionFetcher(url) {
876
+ const TIMEOUT_MS = 10000;
877
+ // Each attempt gets its own AbortController so a timeout on attempt 1 does
878
+ // not bleed into the retry.
879
+ const attemptFetch = (sessionId) => {
880
+ const controller = new AbortController();
881
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
882
+ return fetch(url, {
883
+ method: 'POST',
884
+ headers: { 'Content-Type': 'application/json' },
885
+ body: JSON.stringify({ sessionId }),
886
+ signal: controller.signal,
887
+ }).finally(() => clearTimeout(timer));
888
+ };
889
+ return async (sessionId) => {
890
+ let res;
891
+ try {
892
+ res = await attemptFetch(sessionId);
893
+ }
894
+ catch (firstErr) {
895
+ // Timeout/abort — don't retry, we already waited the full duration.
896
+ if (firstErr instanceof Error && (firstErr.name === 'AbortError' || firstErr.name === 'TimeoutError')) {
897
+ throw new OzError(`Session endpoint timed out after ${TIMEOUT_MS / 1000}s (${url})`, undefined, 'timeout');
898
+ }
899
+ // Pure network error — retry once after a short pause.
900
+ await new Promise(resolve => setTimeout(resolve, 750));
901
+ try {
902
+ res = await attemptFetch(sessionId);
903
+ }
904
+ catch (retryErr) {
905
+ const msg = retryErr instanceof Error ? retryErr.message : 'Network error';
906
+ throw new OzError(`Could not reach session endpoint (${url}): ${msg}`, undefined, 'network');
907
+ }
908
+ }
909
+ // Parse JSON separately from the ok-check so that a non-JSON error body
910
+ // (HTML error page, WAF block, CDN 503) produces the right error code.
911
+ // Previously res.json() was attempted before res.ok was checked; a parse
912
+ // failure on a 5xx HTML body would fall through as {} and produce a
913
+ // misleading 'validation' code when the real cause is a server/network issue.
914
+ let data = {};
915
+ try {
916
+ data = await res.json();
917
+ }
918
+ catch (_a) {
919
+ if (!res.ok) {
920
+ throw new OzError(`Session endpoint returned HTTP ${res.status} with a non-JSON body`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
921
+ }
922
+ // HTTP 200 but body isn't JSON — this is a misconfigured session endpoint.
923
+ throw new OzError('Session endpoint returned HTTP 200 but the response body is not valid JSON. Check your /api/oz-session implementation.', undefined, 'validation');
924
+ }
925
+ if (!res.ok) {
926
+ throw new OzError(typeof data.error === 'string' && data.error
927
+ ? data.error
928
+ : `Session endpoint returned HTTP ${res.status}`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
929
+ }
930
+ // Accept both new `sessionKey` and legacy `waxKey` for backward compatibility
931
+ // with backends that haven't migrated to createSessionMiddleware yet.
932
+ const key = (typeof data.sessionKey === 'string' ? data.sessionKey : '') ||
933
+ (typeof data.waxKey === 'string' ? data.waxKey : '');
934
+ if (!key.trim()) {
935
+ throw new OzError('Session endpoint response is missing sessionKey. Check your /api/oz-session implementation.', undefined, 'validation');
936
+ }
937
+ return key;
938
+ };
939
+ }
940
+
841
941
  function isCardMetadata(v) {
842
- return !!v && typeof v === 'object' && typeof v.last4 === 'string';
942
+ if (!v || typeof v !== 'object')
943
+ return false;
944
+ const r = v;
945
+ return (typeof r.last4 === 'string' &&
946
+ typeof r.brand === 'string' &&
947
+ typeof r.expMonth === 'string' &&
948
+ typeof r.expYear === 'string');
843
949
  }
844
950
  function isBankAccountMetadata(v) {
845
- return !!v && typeof v === 'object' && typeof v.last4 === 'string';
951
+ if (!v || typeof v !== 'object')
952
+ return false;
953
+ const r = v;
954
+ return typeof r.last4 === 'string' && typeof r.routingNumberLast4 === 'string';
846
955
  }
847
- const DEFAULT_FRAME_BASE_URL = "https://elements.ozura.com";
956
+ const DEFAULT_FRAME_BASE_URL = "https://lively-hill-097170c0f.4.azurestaticapps.net";
848
957
  /**
849
958
  * The main entry point for OzElements. Creates and manages iframe-based
850
959
  * card input elements that keep raw card data isolated from the merchant page.
@@ -852,16 +961,10 @@ const DEFAULT_FRAME_BASE_URL = "https://elements.ozura.com";
852
961
  * Use the static `OzVault.create()` factory — do not call `new OzVault()` directly.
853
962
  *
854
963
  * @example
964
+ * // Recommended — pass sessionUrl and let the SDK call your backend automatically
855
965
  * const vault = await OzVault.create({
856
- * pubKey: 'pk_live_...',
857
- * fetchWaxKey: async (sessionId) => {
858
- * // Call your backend — which calls ozura.mintWaxKey() from @ozura/elements/server
859
- * const { waxKey } = await fetch('/api/mint-wax', {
860
- * method: 'POST',
861
- * body: JSON.stringify({ sessionId }),
862
- * }).then(r => r.json());
863
- * return waxKey;
864
- * },
966
+ * pubKey: 'pk_prod_...', // or 'pk_test_...' for test mode
967
+ * sessionUrl: '/api/oz-session', // backend endpoint that calls ozura.createSession()
865
968
  * });
866
969
  * const cardNum = vault.createElement('cardNumber');
867
970
  * cardNum.mount('#card-number');
@@ -877,7 +980,7 @@ class OzVault {
877
980
  * @internal
878
981
  */
879
982
  constructor(options, waxKey, tokenizationSessionId) {
880
- var _a, _b, _c, _d;
983
+ var _a, _b, _c, _d, _e;
881
984
  this.elements = new Map();
882
985
  this.elementsByType = new Map();
883
986
  this.bankElementsByType = new Map();
@@ -911,7 +1014,12 @@ class OzVault {
911
1014
  this.fonts = (_a = options.fonts) !== null && _a !== void 0 ? _a : [];
912
1015
  this.resolvedAppearance = resolveAppearance(options.appearance);
913
1016
  this.vaultId = `vault-${uuid()}`;
914
- this._maxTokenizeCalls = (_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3;
1017
+ // sessionLimit takes precedence over legacy maxTokenizeCalls.
1018
+ // null means unlimited — use Infinity so the ">=" check never triggers.
1019
+ const rawLimit = options.sessionLimit !== undefined
1020
+ ? options.sessionLimit
1021
+ : ((_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3);
1022
+ this._maxTokenizeCalls = rawLimit === null ? Infinity : rawLimit;
915
1023
  this._debug = (_c = options.debug) !== null && _c !== void 0 ? _c : false;
916
1024
  this.boundHandleMessage = this.handleMessage.bind(this);
917
1025
  window.addEventListener('message', this.boundHandleMessage);
@@ -927,7 +1035,8 @@ class OzVault {
927
1035
  }
928
1036
  }, timeout);
929
1037
  }
930
- this._onWaxRefresh = options.onWaxRefresh;
1038
+ // onSessionRefresh takes precedence over legacy onWaxRefresh
1039
+ this._onWaxRefresh = (_e = options.onSessionRefresh) !== null && _e !== void 0 ? _e : options.onWaxRefresh;
931
1040
  this._onReady = options.onReady;
932
1041
  this.log('vault created', { vaultId: this.vaultId, frameBaseUrl: this.frameBaseUrl, maxTokenizeCalls: this._maxTokenizeCalls });
933
1042
  }
@@ -935,51 +1044,63 @@ class OzVault {
935
1044
  * Creates and returns a ready `OzVault` instance.
936
1045
  *
937
1046
  * Internally this:
938
- * 1. Generates a `tokenizationSessionId` (UUID).
1047
+ * 1. Generates a session UUID.
939
1048
  * 2. Starts loading the hidden tokenizer iframe immediately.
940
- * 3. Calls `options.fetchWaxKey(tokenizationSessionId)` concurrently — your
941
- * backend mints a session-bound wax key from the vault and returns it.
942
- * 4. Resolves with the vault instance once the wax key is stored. The iframe
1049
+ * 3. Fetches a session key from your backend concurrently — either via
1050
+ * `sessionUrl` (simplest), `getSessionKey` (custom headers/auth), or the
1051
+ * deprecated `fetchWaxKey` callback.
1052
+ * 4. Resolves with the vault instance once the session key is stored. The iframe
943
1053
  * has been loading the whole time, so `isReady` may already be true or
944
1054
  * will fire shortly after.
945
1055
  *
946
1056
  * The returned vault is ready to create elements immediately. `createToken()`
947
1057
  * additionally requires `vault.isReady` (tokenizer iframe loaded).
948
1058
  *
949
- * @throws {OzError} if `fetchWaxKey` throws, returns a non-string value, or returns an empty/whitespace-only string.
1059
+ * @throws {OzError} if the session fetch fails, times out, or returns an empty string.
950
1060
  */
951
1061
  static async create(options, signal) {
952
1062
  if (!options.pubKey || !options.pubKey.trim()) {
953
1063
  throw new OzError('pubKey is required in options. Obtain your public key from the Ozura admin.');
954
1064
  }
955
- if (typeof options.fetchWaxKey !== 'function') {
956
- throw new OzError('fetchWaxKey must be a function. See OzVault.create() docs for the expected signature.');
1065
+ // Normalize the session callback. Priority: sessionUrl > getSessionKey > fetchWaxKey (deprecated).
1066
+ // This allows merchants to use the clean new API without touching legacy code.
1067
+ let resolvedFetchKey;
1068
+ if (options.sessionUrl) {
1069
+ resolvedFetchKey = createSessionFetcher(options.sessionUrl);
1070
+ }
1071
+ else if (typeof options.getSessionKey === 'function') {
1072
+ resolvedFetchKey = options.getSessionKey;
1073
+ }
1074
+ else if (typeof options.fetchWaxKey === 'function') {
1075
+ resolvedFetchKey = options.fetchWaxKey;
1076
+ }
1077
+ else {
1078
+ throw new OzError('A session URL or callback is required. Pass sessionUrl, getSessionKey, or fetchWaxKey to OzVault.create().');
957
1079
  }
958
1080
  const tokenizationSessionId = uuid();
959
1081
  // Construct the vault immediately — this mounts the tokenizer iframe so it
960
- // starts loading while fetchWaxKey is in flight. The waxKey field starts
961
- // empty and is set below before create() returns.
1082
+ // starts loading while the session fetch is in flight.
962
1083
  const vault = new OzVault(options, '', tokenizationSessionId);
963
1084
  // If the caller provides an AbortSignal (e.g. React useEffect cleanup),
964
1085
  // destroy the vault immediately on abort so the tokenizer iframe and message
965
- // listener are removed synchronously rather than waiting for fetchWaxKey to
966
- // settle. This eliminates the brief double-iframe window in React StrictMode.
1086
+ // listener are removed synchronously rather than waiting for the session fetch
1087
+ // to settle. This eliminates the brief double-iframe window in React StrictMode.
967
1088
  const onAbort = () => vault.destroy();
968
1089
  signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onAbort, { once: true });
969
1090
  let waxKey;
970
1091
  try {
971
- waxKey = await options.fetchWaxKey(tokenizationSessionId);
1092
+ waxKey = await resolvedFetchKey(tokenizationSessionId);
972
1093
  }
973
1094
  catch (err) {
974
1095
  signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
975
1096
  vault.destroy();
976
1097
  if (signal === null || signal === void 0 ? void 0 : signal.aborted)
977
1098
  throw new OzError('OzVault.create() was cancelled.');
978
- // Preserve errorCode/retryable from OzError (e.g. timeout/network from createFetchWaxKey)
1099
+ // Preserve errorCode/retryable from OzError (e.g. timeout/network from createSessionFetcher)
979
1100
  // so callers can distinguish transient failures from config errors.
980
1101
  const originalCode = err instanceof OzError ? err.errorCode : undefined;
981
1102
  const msg = err instanceof Error ? err.message : 'Unknown error';
982
- throw new OzError(`fetchWaxKey threw an error: ${msg}`, undefined, originalCode);
1103
+ throw new OzError(`Session fetch threw an error: ${msg}`, undefined, originalCode);
983
1104
  }
984
1105
  signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
985
1106
  if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
@@ -988,11 +1109,11 @@ class OzVault {
988
1109
  }
989
1110
  if (typeof waxKey !== 'string' || !waxKey.trim()) {
990
1111
  vault.destroy();
991
- throw new OzError('fetchWaxKey must return a non-empty wax key string. Check your mint endpoint.');
1112
+ throw new OzError('Session fetch returned an empty key. Check your session endpoint response — it must return { sessionKey: "..." }.');
992
1113
  }
993
1114
  // Static methods can access private fields of instances of the same class.
994
1115
  vault.waxKey = waxKey;
995
- vault._storedFetchWaxKey = options.fetchWaxKey;
1116
+ vault._storedFetchWaxKey = resolvedFetchKey;
996
1117
  // If the tokenizer iframe fired OZ_FRAME_READY before fetchWaxKey resolved,
997
1118
  // the OZ_INIT sent at that point had an empty waxKey. Send a follow-up now
998
1119
  // so the tokenizer has the key stored before any createToken() call.
@@ -1079,7 +1200,12 @@ class OzVault {
1079
1200
  this.completionState.delete(existing.frameId);
1080
1201
  existing.destroy();
1081
1202
  }
1082
- const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance);
1203
+ const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance, () => {
1204
+ // Prune vault-level maps when the element is manually destroyed so they
1205
+ // don't grow unboundedly in SPA scenarios with repeated mount/unmount cycles.
1206
+ this.elements.delete(el.frameId);
1207
+ this.completionState.delete(el.frameId);
1208
+ });
1083
1209
  this.elements.set(el.frameId, el);
1084
1210
  typeMap.set(type, el);
1085
1211
  return el;
@@ -1658,9 +1784,9 @@ class OzVault {
1658
1784
  // key without waiting for a vault rejection.
1659
1785
  this._tokenizeSuccessCount++;
1660
1786
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1661
- this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1787
+ this.log('proactive session key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1662
1788
  this.refreshWaxKey().catch((err) => {
1663
- console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1789
+ console.warn('[OzVault] Post-budget session key refresh failed:', err instanceof Error ? err.message : err);
1664
1790
  });
1665
1791
  }
1666
1792
  }
@@ -1736,61 +1862,65 @@ class OzVault {
1736
1862
  }
1737
1863
  pending.reject(new OzError(normalizeVaultError(raw), raw, errorCode));
1738
1864
  }
1739
- // Also check bank resolvers — both card and bank errors use OZ_TOKEN_ERROR
1740
- const bankPending = this.bankTokenizeResolvers.get(msg.requestId);
1741
- if (bankPending) {
1742
- this.bankTokenizeResolvers.delete(msg.requestId);
1743
- if (bankPending.timeoutId != null)
1744
- clearTimeout(bankPending.timeoutId);
1745
- if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
1746
- const resetCountAtRetry = this._resetCount;
1747
- this.refreshWaxKey().then(() => {
1748
- if (this._destroyed) {
1749
- bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
1750
- return;
1751
- }
1752
- if (this._resetCount !== resetCountAtRetry) {
1753
- bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1754
- return;
1755
- }
1756
- const newRequestId = `req-${uuid()}`;
1757
- this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
1758
- try {
1759
- const retryBankChannels = bankPending.readyElements.map(() => new MessageChannel());
1760
- this.sendToTokenizer({
1761
- type: 'OZ_BANK_TOKENIZE',
1762
- requestId: newRequestId,
1763
- waxKey: this.waxKey,
1764
- tokenizationSessionId: this.tokenizationSessionId,
1765
- pubKey: this.pubKey,
1766
- firstName: bankPending.firstName,
1767
- lastName: bankPending.lastName,
1768
- fieldCount: bankPending.fieldCount,
1769
- }, retryBankChannels.map(ch => ch.port1));
1770
- bankPending.readyElements.forEach((el, i) => el.beginCollect(newRequestId, retryBankChannels[i].port2));
1771
- const retryBankTimeoutId = setTimeout(() => {
1772
- if (this.bankTokenizeResolvers.has(newRequestId)) {
1773
- this.bankTokenizeResolvers.delete(newRequestId);
1774
- this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId: newRequestId });
1775
- bankPending.reject(new OzError('Bank tokenization timed out after wax key refresh.', undefined, 'timeout'));
1776
- }
1777
- }, 30000);
1778
- const retryBankEntry = this.bankTokenizeResolvers.get(newRequestId);
1779
- if (retryBankEntry)
1780
- retryBankEntry.timeoutId = retryBankTimeoutId;
1781
- }
1782
- catch (setupErr) {
1783
- this.bankTokenizeResolvers.delete(newRequestId);
1784
- bankPending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry bank tokenization failed to start'));
1785
- }
1786
- }).catch((refreshErr) => {
1787
- const msg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
1788
- bankPending.reject(new OzError(msg, undefined, 'auth'));
1789
- });
1790
- break;
1865
+ else {
1866
+ // Also check bank resolvers — both card and bank errors use OZ_TOKEN_ERROR.
1867
+ // Use else-if rather than sequential checks so a UUID collision (however
1868
+ // improbable) can never trigger double-rejection of two unrelated resolvers.
1869
+ const bankPending = this.bankTokenizeResolvers.get(msg.requestId);
1870
+ if (bankPending) {
1871
+ this.bankTokenizeResolvers.delete(msg.requestId);
1872
+ if (bankPending.timeoutId != null)
1873
+ clearTimeout(bankPending.timeoutId);
1874
+ if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
1875
+ const resetCountAtRetry = this._resetCount;
1876
+ this.refreshWaxKey().then(() => {
1877
+ if (this._destroyed) {
1878
+ bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
1879
+ return;
1880
+ }
1881
+ if (this._resetCount !== resetCountAtRetry) {
1882
+ bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1883
+ return;
1884
+ }
1885
+ const newRequestId = `req-${uuid()}`;
1886
+ this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
1887
+ try {
1888
+ const retryBankChannels = bankPending.readyElements.map(() => new MessageChannel());
1889
+ this.sendToTokenizer({
1890
+ type: 'OZ_BANK_TOKENIZE',
1891
+ requestId: newRequestId,
1892
+ waxKey: this.waxKey,
1893
+ tokenizationSessionId: this.tokenizationSessionId,
1894
+ pubKey: this.pubKey,
1895
+ firstName: bankPending.firstName,
1896
+ lastName: bankPending.lastName,
1897
+ fieldCount: bankPending.fieldCount,
1898
+ }, retryBankChannels.map(ch => ch.port1));
1899
+ bankPending.readyElements.forEach((el, i) => el.beginCollect(newRequestId, retryBankChannels[i].port2));
1900
+ const retryBankTimeoutId = setTimeout(() => {
1901
+ if (this.bankTokenizeResolvers.has(newRequestId)) {
1902
+ this.bankTokenizeResolvers.delete(newRequestId);
1903
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId: newRequestId });
1904
+ bankPending.reject(new OzError('Bank tokenization timed out after wax key refresh.', undefined, 'timeout'));
1905
+ }
1906
+ }, 30000);
1907
+ const retryBankEntry = this.bankTokenizeResolvers.get(newRequestId);
1908
+ if (retryBankEntry)
1909
+ retryBankEntry.timeoutId = retryBankTimeoutId;
1910
+ }
1911
+ catch (setupErr) {
1912
+ this.bankTokenizeResolvers.delete(newRequestId);
1913
+ bankPending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry bank tokenization failed to start'));
1914
+ }
1915
+ }).catch((refreshErr) => {
1916
+ const msg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
1917
+ bankPending.reject(new OzError(msg, undefined, 'auth'));
1918
+ });
1919
+ break;
1920
+ }
1921
+ bankPending.reject(new OzError(normalizeBankVaultError(raw), raw, errorCode));
1791
1922
  }
1792
- bankPending.reject(new OzError(normalizeBankVaultError(raw), raw, errorCode));
1793
- }
1923
+ } // end else (bank path)
1794
1924
  break;
1795
1925
  }
1796
1926
  case 'OZ_BANK_TOKEN_RESULT': {
@@ -1818,9 +1948,9 @@ class OzVault {
1818
1948
  // Same proactive refresh logic as card tokenization.
1819
1949
  this._tokenizeSuccessCount++;
1820
1950
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1821
- this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1951
+ this.log('proactive session key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1822
1952
  this.refreshWaxKey().catch((err) => {
1823
- console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1953
+ console.warn('[OzVault] Post-budget session key refresh failed:', err instanceof Error ? err.message : err);
1824
1954
  });
1825
1955
  }
1826
1956
  }
@@ -1906,84 +2036,6 @@ class OzVault {
1906
2036
  }
1907
2037
  }
1908
2038
 
1909
- /**
1910
- * Creates a ready-to-use `fetchWaxKey` callback for `OzVault.create()` and `<OzElements>`.
1911
- *
1912
- * Calls your backend mint endpoint with `{ sessionId }` and returns the wax key string.
1913
- * Throws on non-OK responses or a missing `waxKey` field so the vault can surface the
1914
- * error through its normal error path.
1915
- *
1916
- * Each call enforces a 10-second per-attempt timeout. On a pure network-level
1917
- * failure (connection refused, DNS failure, etc.) the call is retried once after
1918
- * 750ms before throwing. HTTP errors (4xx/5xx) are never retried — they indicate
1919
- * an endpoint misconfiguration or an invalid key, not a transient failure.
1920
- *
1921
- * The mint endpoint is typically the one-line `createMintWaxHandler` / `createMintWaxMiddleware`
1922
- * from `@ozura/elements/server`.
1923
- *
1924
- * @param mintUrl - Absolute or relative URL of your wax-key mint endpoint, e.g. `'/api/mint-wax'`.
1925
- *
1926
- * @example
1927
- * // Vanilla JS
1928
- * const vault = await OzVault.create({
1929
- * pubKey: 'pk_live_...',
1930
- * fetchWaxKey: createFetchWaxKey('/api/mint-wax'),
1931
- * });
1932
- *
1933
- * @example
1934
- * // React
1935
- * <OzElements pubKey="pk_live_..." fetchWaxKey={createFetchWaxKey('/api/mint-wax')}>
1936
- */
1937
- function createFetchWaxKey(mintUrl) {
1938
- const TIMEOUT_MS = 10000;
1939
- // Each attempt gets its own AbortController so a timeout on attempt 1 does
1940
- // not bleed into the retry. Uses AbortController + setTimeout instead of
1941
- // AbortSignal.timeout() to support environments without that API.
1942
- const attemptFetch = (sessionId) => {
1943
- const controller = new AbortController();
1944
- const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
1945
- return fetch(mintUrl, {
1946
- method: 'POST',
1947
- headers: { 'Content-Type': 'application/json' },
1948
- body: JSON.stringify({ sessionId }),
1949
- signal: controller.signal,
1950
- }).finally(() => clearTimeout(timer));
1951
- };
1952
- return async (sessionId) => {
1953
- let res;
1954
- try {
1955
- res = await attemptFetch(sessionId);
1956
- }
1957
- catch (firstErr) {
1958
- // Abort/timeout should not be retried — the server received nothing or
1959
- // we already waited the full timeout duration.
1960
- if (firstErr instanceof Error && (firstErr.name === 'AbortError' || firstErr.name === 'TimeoutError')) {
1961
- throw new OzError(`Wax key mint timed out after ${TIMEOUT_MS / 1000}s (${mintUrl})`, undefined, 'timeout');
1962
- }
1963
- // Pure network error (offline, DNS, connection refused) — retry once
1964
- // after a short pause in case of a transient blip.
1965
- await new Promise(resolve => setTimeout(resolve, 750));
1966
- try {
1967
- res = await attemptFetch(sessionId);
1968
- }
1969
- catch (retryErr) {
1970
- const msg = retryErr instanceof Error ? retryErr.message : 'Network error';
1971
- throw new OzError(`Could not reach wax key mint endpoint (${mintUrl}): ${msg}`, undefined, 'network');
1972
- }
1973
- }
1974
- const data = await res.json().catch(() => ({}));
1975
- if (!res.ok) {
1976
- throw new OzError(typeof data.error === 'string' && data.error
1977
- ? data.error
1978
- : `Wax key mint failed (HTTP ${res.status})`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
1979
- }
1980
- if (typeof data.waxKey !== 'string' || !data.waxKey.trim()) {
1981
- throw new OzError('Mint endpoint response is missing waxKey. Check your /api/mint-wax implementation.', undefined, 'validation');
1982
- }
1983
- return data.waxKey;
1984
- };
1985
- }
1986
-
1987
2039
  const OzContext = createContext({
1988
2040
  vault: null,
1989
2041
  initError: null,
@@ -2000,7 +2052,7 @@ const OzContext = createContext({
2000
2052
  * All `<OzCardNumber />`, `<OzExpiry />`, and `<OzCvv />` children must be
2001
2053
  * rendered inside this provider.
2002
2054
  */
2003
- function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loadTimeoutMs, onWaxRefresh, onReady, appearance, maxTokenizeCalls, debug, children }) {
2055
+ function OzElements({ sessionUrl, getSessionKey, fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loadTimeoutMs, onSessionRefresh, onWaxRefresh, onReady, appearance, sessionLimit, maxTokenizeCalls, debug, children }) {
2004
2056
  const [vault, setVault] = useState(null);
2005
2057
  const [initError, setInitError] = useState(null);
2006
2058
  const [mountedCount, setMountedCount] = useState(0);
@@ -2008,13 +2060,14 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
2008
2060
  const [tokenizeCount, setTokenizeCount] = useState(0);
2009
2061
  const onLoadErrorRef = useRef(onLoadError);
2010
2062
  onLoadErrorRef.current = onLoadError;
2011
- const onWaxRefreshRef = useRef(onWaxRefresh);
2012
- onWaxRefreshRef.current = onWaxRefresh;
2063
+ const onWaxRefreshRef = useRef(onSessionRefresh !== null && onSessionRefresh !== void 0 ? onSessionRefresh : onWaxRefresh);
2064
+ onWaxRefreshRef.current = onSessionRefresh !== null && onSessionRefresh !== void 0 ? onSessionRefresh : onWaxRefresh;
2013
2065
  const onReadyRef = useRef(onReady);
2014
2066
  onReadyRef.current = onReady;
2015
- // Keep a ref to fetchWaxKey so changes don't trigger vault recreation
2016
- const fetchWaxKeyRef = useRef(fetchWaxKey);
2017
- fetchWaxKeyRef.current = fetchWaxKey;
2067
+ // Keep a ref to the session callback so changes don't trigger vault recreation.
2068
+ // Priority mirrors OzVault.create(): sessionUrl > getSessionKey > fetchWaxKey.
2069
+ const getSessionKeyRef = useRef(getSessionKey !== null && getSessionKey !== void 0 ? getSessionKey : fetchWaxKey);
2070
+ getSessionKeyRef.current = getSessionKey !== null && getSessionKey !== void 0 ? getSessionKey : fetchWaxKey;
2018
2071
  const appearanceKey = useMemo(() => appearance ? JSON.stringify(appearance) : '', [appearance]);
2019
2072
  const fontsKey = useMemo(() => fonts ? JSON.stringify(fonts) : '', [fonts]);
2020
2073
  useEffect(() => {
@@ -2041,7 +2094,11 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
2041
2094
  // synchronously rather than waiting for the promise to settle. Without this,
2042
2095
  // two hidden iframes and two window listeners briefly coexist.
2043
2096
  const abortController = new AbortController();
2044
- OzVault.create(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ pubKey, fetchWaxKey: (sessionId) => fetchWaxKeyRef.current(sessionId) }, (frameBaseUrl ? { frameBaseUrl } : {})), (parsedFonts ? { fonts: parsedFonts } : {})), (parsedAppearance ? { appearance: parsedAppearance } : {})), (onLoadErrorRef.current ? { onLoadError: fireLoadError, loadTimeoutMs } : {})), {
2097
+ OzVault.create(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ pubKey }, (sessionUrl
2098
+ ? { sessionUrl }
2099
+ : getSessionKeyRef.current
2100
+ ? { getSessionKey: (sessionId) => getSessionKeyRef.current(sessionId) }
2101
+ : {})), (frameBaseUrl ? { frameBaseUrl } : {})), (parsedFonts ? { fonts: parsedFonts } : {})), (parsedAppearance ? { appearance: parsedAppearance } : {})), (onLoadErrorRef.current ? { onLoadError: fireLoadError, loadTimeoutMs } : {})), {
2045
2102
  // Always install onWaxRefresh internally so we can reset tokenizeCount
2046
2103
  // when any wax key refresh occurs (reactive TTL expiry, post-budget
2047
2104
  // proactive, or visibility-change proactive). Without this the React
@@ -2063,11 +2120,12 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
2063
2120
  // the retry tokenization completes (a full fetchWaxKey + tokenize round-
2064
2121
  // trip separates them), so the count correctly resets to 0 then rises to
2065
2122
  // 1 after the retry notifyTokenize fires.
2066
- onWaxRefresh: () => {
2123
+ onSessionRefresh: () => {
2067
2124
  var _a;
2068
2125
  Promise.resolve().then(() => setTokenizeCount(0));
2069
2126
  (_a = onWaxRefreshRef.current) === null || _a === void 0 ? void 0 : _a.call(onWaxRefreshRef);
2070
- }, onReady: () => { var _a; return (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef); } }), (maxTokenizeCalls !== undefined ? { maxTokenizeCalls } : {})), (debug ? { debug: true } : {})), abortController.signal).then(v => {
2127
+ }, onReady: () => { var _a; return (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef); } }), (sessionLimit !== undefined ? { sessionLimit }
2128
+ : maxTokenizeCalls !== undefined ? { maxTokenizeCalls } : {})), (debug ? { debug: true } : {})), abortController.signal).then(v => {
2071
2129
  if (cancelled) {
2072
2130
  v.destroy();
2073
2131
  return;
@@ -2100,7 +2158,7 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
2100
2158
  setVault(null);
2101
2159
  setInitError(null);
2102
2160
  };
2103
- }, [pubKey, frameBaseUrl, loadTimeoutMs, appearanceKey, fontsKey, maxTokenizeCalls, debug]);
2161
+ }, [pubKey, sessionUrl, frameBaseUrl, loadTimeoutMs, appearanceKey, fontsKey, sessionLimit, maxTokenizeCalls, debug]);
2104
2162
  const notifyMount = useCallback(() => setMountedCount(n => n + 1), []);
2105
2163
  const notifyReady = useCallback(() => setReadyCount(n => n + 1), []);
2106
2164
  const notifyUnmount = useCallback(() => {
@@ -2436,5 +2494,5 @@ function OzBankCard({ style, styles, classNames, labels, labelStyle, labelClassN
2436
2494
  return (jsxs("div", { className: className, style: { width: '100%', display: 'flex', flexDirection: 'column', gap: gapStr }, children: [jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.accountNumber), jsx(OzBankAccountNumber, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.accountNumber), className: classNames === null || classNames === void 0 ? void 0 : classNames.accountNumber, placeholder: (_a = placeholders === null || placeholders === void 0 ? void 0 : placeholders.accountNumber) !== null && _a !== void 0 ? _a : 'Account number', disabled: disabled, onChange: (e) => { fieldState.current.accountNumber = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'accountNumber'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'accountNumber'); }, onReady: readyHandlers['accountNumber'] })] }), jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.routingNumber), jsx(OzBankRoutingNumber, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.routingNumber), className: classNames === null || classNames === void 0 ? void 0 : classNames.routingNumber, placeholder: (_b = placeholders === null || placeholders === void 0 ? void 0 : placeholders.routingNumber) !== null && _b !== void 0 ? _b : 'Routing number', disabled: disabled, onChange: (e) => { fieldState.current.routingNumber = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'routingNumber'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'routingNumber'); }, onReady: readyHandlers['routingNumber'] })] }), errorNode] }));
2437
2495
  }
2438
2496
 
2439
- export { OzBankAccountNumber, OzBankCard, OzBankRoutingNumber, OzCard, OzCardNumber, OzCvv, OzElements, OzExpiry, createFetchWaxKey, useOzElements };
2497
+ export { OzBankAccountNumber, OzBankCard, OzBankRoutingNumber, OzCard, OzCardNumber, OzCvv, OzElements, OzExpiry, createSessionFetcher as createFetchWaxKey, createSessionFetcher, useOzElements };
2440
2498
  //# sourceMappingURL=index.esm.js.map