@ozura/elements 1.1.0-next.21 → 1.1.0-next.23

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.
@@ -4,147 +4,6 @@
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.OzElements = {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
- /**
8
- * errors.ts — error types and normalisation for OzElements.
9
- *
10
- * Three normalisation functions:
11
- * - normalizeVaultError — maps raw vault /tokenize errors to user-facing messages (card flows)
12
- * - normalizeBankVaultError — maps raw vault /tokenize errors to user-facing messages (bank/ACH flows)
13
- * - normalizeCardSaleError — maps raw cardSale API errors to user-facing messages
14
- *
15
- * Error keys in normalizeCardSaleError are taken directly from checkout's
16
- * errorMapping.ts so the same error strings produce the same user-facing copy.
17
- */
18
- const OZ_ERROR_CODES = new Set(['network', 'timeout', 'auth', 'validation', 'server', 'config', 'unknown']);
19
- /** Returns true and narrows to OzErrorCode when `value` is a valid member of the union. */
20
- function isOzErrorCode(value) {
21
- return typeof value === 'string' && OZ_ERROR_CODES.has(value);
22
- }
23
- class OzError extends Error {
24
- constructor(message, raw, errorCode) {
25
- super(message);
26
- this.name = 'OzError';
27
- this.raw = raw !== null && raw !== void 0 ? raw : message;
28
- this.errorCode = errorCode !== null && errorCode !== void 0 ? errorCode : 'unknown';
29
- this.retryable = this.errorCode === 'network' || this.errorCode === 'timeout' || this.errorCode === 'server';
30
- }
31
- }
32
- /** Shared patterns that apply to both card and bank vault errors. */
33
- function normalizeCommonVaultError(msg) {
34
- if (msg.includes('api key') || msg.includes('unauthorized') || msg.includes('authentication') || msg.includes('forbidden')) {
35
- return 'Authentication failed. Check your vault API key configuration.';
36
- }
37
- if (msg.includes('network') || msg.includes('failed to fetch') || msg.includes('networkerror')) {
38
- return 'A network error occurred. Please check your connection and try again.';
39
- }
40
- if (msg.includes('timeout') || msg.includes('timed out')) {
41
- return 'The request timed out. Please try again.';
42
- }
43
- if (msg.includes('http 5') || msg.includes('500') || msg.includes('502') || msg.includes('503')) {
44
- return 'A server error occurred. Please try again shortly.';
45
- }
46
- return null;
47
- }
48
- /**
49
- * Maps a raw vault /tokenize error string to a user-facing message for card flows.
50
- * Falls back to the original string if no pattern matches.
51
- */
52
- function normalizeVaultError(raw) {
53
- const msg = raw.toLowerCase();
54
- if (msg.includes('card number') || msg.includes('invalid card') || msg.includes('luhn')) {
55
- return 'The card number is invalid. Please check and try again.';
56
- }
57
- if (msg.includes('expir')) {
58
- return 'The card expiration date is invalid or the card has expired.';
59
- }
60
- if (msg.includes('cvv') || msg.includes('cvc') || msg.includes('security code')) {
61
- return 'The CVV code is invalid. Please check and try again.';
62
- }
63
- if (msg.includes('insufficient') || msg.includes('funds')) {
64
- return 'Your card has insufficient funds. Please use a different card.';
65
- }
66
- if (msg.includes('declined') || msg.includes('do not honor')) {
67
- return 'Your card was declined. Please try a different card or contact your bank.';
68
- }
69
- const common = normalizeCommonVaultError(msg);
70
- if (common)
71
- return common;
72
- return raw;
73
- }
74
- /**
75
- * Maps a raw vault /tokenize error string to a user-facing message for bank (ACH) flows.
76
- * Uses bank-specific pattern matching so card-specific messages are never shown for
77
- * bank errors. Falls back to the original string if no pattern matches.
78
- */
79
- function normalizeBankVaultError(raw) {
80
- const msg = raw.toLowerCase();
81
- if (msg.includes('account number') || msg.includes('account_number') || msg.includes('invalid account')) {
82
- return 'The bank account number is invalid. Please check and try again.';
83
- }
84
- if (msg.includes('routing number') || msg.includes('routing_number') || msg.includes('invalid routing') || /\baba\b/.test(msg)) {
85
- return 'The routing number is invalid. Please check and try again.';
86
- }
87
- const common = normalizeCommonVaultError(msg);
88
- if (common)
89
- return common;
90
- return raw;
91
- }
92
- // ─── cardSale error map (mirrors checkout/src/utils/errorMapping.ts exactly) ─
93
- const CARD_SALE_ERROR_MAP = [
94
- ['Insufficient Funds', 'Your card has insufficient funds. Please use a different payment method.'],
95
- ['Invalid card number', 'The card number you entered is invalid. Please check and try again.'],
96
- ['Card expired', 'Your card has expired. Please use a different card.'],
97
- ['CVV Verification Failed', 'The CVV code you entered is incorrect. Please check and try again.'],
98
- ['Address Verification Failed', 'The billing address does not match your card. Please verify your address.'],
99
- ['Do Not Honor', 'Your card was declined. Please contact your bank or use a different payment method.'],
100
- ['Declined', 'Your card was declined. Please contact your bank or use a different payment method.'],
101
- ['Surcharge is currently not supported', 'Surcharge fees are not supported at this time.'],
102
- ['Surcharge percent must be between', 'Surcharge fees are not supported at this time.'],
103
- ['Forbidden - API key', 'Authentication failed. Please refresh the page.'],
104
- ['Api Key is invalid', 'Authentication failed. Please refresh the page.'],
105
- ['API key not found', 'Authentication failed. Please refresh the page.'],
106
- ['Access token expired', 'Your session has expired. Please refresh the page.'],
107
- ['Access token is invalid', 'Authentication failed. Please refresh the page.'],
108
- ['Unauthorized', 'Authentication failed. Please refresh the page.'],
109
- ['Too Many Requests', 'Too many requests. Please wait a moment and try again.'],
110
- ['Rate limit exceeded', 'System is busy. Please wait a moment and try again.'],
111
- ['No processor integrations found', 'Payment processing is not configured for this merchant. Please contact the merchant for assistance.'],
112
- ['processor integration', 'Payment processing is temporarily unavailable. Please try again later or contact the merchant.'],
113
- ['Invalid zipcode', 'The ZIP code you entered is invalid. Please check and try again.'],
114
- ['failed to save to database', 'Your payment was processed but we encountered an issue. Please contact support.'],
115
- ['CASHBACK UNAVAIL', 'This transaction type is not supported. Please try again or use a different payment method.'],
116
- ];
117
- /**
118
- * Maps a raw Ozura Pay API cardSale error string to a user-facing message.
119
- *
120
- * Uses the exact same error key table as checkout's `getUserFriendlyError()` in
121
- * errorMapping.ts so both surfaces produce identical copy for the same errors.
122
- *
123
- * Falls back to the original string when it's under 100 characters, or to a
124
- * generic message for long/opaque server errors — matching checkout's fallback
125
- * behaviour exactly.
126
- *
127
- * **Trade-off:** Short unrecognised strings (e.g. processor codes like
128
- * `"PROC_TIMEOUT"`) are passed through verbatim. This intentionally mirrors
129
- * checkout so the same raw Pay API errors produce the same user-facing text on
130
- * both surfaces. If the Pay API ever returns internal codes that should never
131
- * reach the UI, the fix belongs in the Pay API error normalisation layer rather
132
- * than here.
133
- */
134
- function normalizeCardSaleError(raw) {
135
- if (!raw)
136
- return 'Payment processing failed. Please try again.';
137
- for (const [key, message] of CARD_SALE_ERROR_MAP) {
138
- if (raw.toLowerCase().includes(key.toLowerCase())) {
139
- return message;
140
- }
141
- }
142
- // Checkout fallback: pass through short errors, genericise long ones
143
- if (raw.length < 100)
144
- return raw;
145
- return 'Payment processing failed. Please try again or contact support.';
146
- }
147
-
148
7
  const THEME_DEFAULT = {
149
8
  base: {
150
9
  color: '#1a1a2e',
@@ -309,6 +168,147 @@
309
168
  return mergeStyleConfigs(appearance, elementStyle);
310
169
  }
311
170
 
171
+ /**
172
+ * errors.ts — error types and normalisation for OzElements.
173
+ *
174
+ * Three normalisation functions:
175
+ * - normalizeVaultError — maps raw vault /tokenize errors to user-facing messages (card flows)
176
+ * - normalizeBankVaultError — maps raw vault /tokenize errors to user-facing messages (bank/ACH flows)
177
+ * - normalizeCardSaleError — maps raw cardSale API errors to user-facing messages
178
+ *
179
+ * Error keys in normalizeCardSaleError are taken directly from checkout's
180
+ * errorMapping.ts so the same error strings produce the same user-facing copy.
181
+ */
182
+ const OZ_ERROR_CODES = new Set(['network', 'timeout', 'auth', 'validation', 'server', 'config', 'unknown']);
183
+ /** Returns true and narrows to OzErrorCode when `value` is a valid member of the union. */
184
+ function isOzErrorCode(value) {
185
+ return typeof value === 'string' && OZ_ERROR_CODES.has(value);
186
+ }
187
+ class OzError extends Error {
188
+ constructor(message, raw, errorCode) {
189
+ super(message);
190
+ this.name = 'OzError';
191
+ this.raw = raw !== null && raw !== void 0 ? raw : message;
192
+ this.errorCode = errorCode !== null && errorCode !== void 0 ? errorCode : 'unknown';
193
+ this.retryable = this.errorCode === 'network' || this.errorCode === 'timeout' || this.errorCode === 'server';
194
+ }
195
+ }
196
+ /** Shared patterns that apply to both card and bank vault errors. */
197
+ function normalizeCommonVaultError(msg) {
198
+ if (msg.includes('api key') || msg.includes('unauthorized') || msg.includes('authentication') || msg.includes('forbidden')) {
199
+ return 'Authentication failed. Check your vault API key configuration.';
200
+ }
201
+ if (msg.includes('network') || msg.includes('failed to fetch') || msg.includes('networkerror')) {
202
+ return 'A network error occurred. Please check your connection and try again.';
203
+ }
204
+ if (msg.includes('timeout') || msg.includes('timed out')) {
205
+ return 'The request timed out. Please try again.';
206
+ }
207
+ if (msg.includes('http 5') || msg.includes('500') || msg.includes('502') || msg.includes('503')) {
208
+ return 'A server error occurred. Please try again shortly.';
209
+ }
210
+ return null;
211
+ }
212
+ /**
213
+ * Maps a raw vault /tokenize error string to a user-facing message for card flows.
214
+ * Falls back to the original string if no pattern matches.
215
+ */
216
+ function normalizeVaultError(raw) {
217
+ const msg = raw.toLowerCase();
218
+ if (msg.includes('card number') || msg.includes('invalid card') || msg.includes('luhn')) {
219
+ return 'The card number is invalid. Please check and try again.';
220
+ }
221
+ if (msg.includes('expir')) {
222
+ return 'The card expiration date is invalid or the card has expired.';
223
+ }
224
+ if (msg.includes('cvv') || msg.includes('cvc') || msg.includes('security code')) {
225
+ return 'The CVV code is invalid. Please check and try again.';
226
+ }
227
+ if (msg.includes('insufficient') || msg.includes('funds')) {
228
+ return 'Your card has insufficient funds. Please use a different card.';
229
+ }
230
+ if (msg.includes('declined') || msg.includes('do not honor')) {
231
+ return 'Your card was declined. Please try a different card or contact your bank.';
232
+ }
233
+ const common = normalizeCommonVaultError(msg);
234
+ if (common)
235
+ return common;
236
+ return raw;
237
+ }
238
+ /**
239
+ * Maps a raw vault /tokenize error string to a user-facing message for bank (ACH) flows.
240
+ * Uses bank-specific pattern matching so card-specific messages are never shown for
241
+ * bank errors. Falls back to the original string if no pattern matches.
242
+ */
243
+ function normalizeBankVaultError(raw) {
244
+ const msg = raw.toLowerCase();
245
+ if (msg.includes('account number') || msg.includes('account_number') || msg.includes('invalid account')) {
246
+ return 'The bank account number is invalid. Please check and try again.';
247
+ }
248
+ if (msg.includes('routing number') || msg.includes('routing_number') || msg.includes('invalid routing') || /\baba\b/.test(msg)) {
249
+ return 'The routing number is invalid. Please check and try again.';
250
+ }
251
+ const common = normalizeCommonVaultError(msg);
252
+ if (common)
253
+ return common;
254
+ return raw;
255
+ }
256
+ // ─── cardSale error map (mirrors checkout/src/utils/errorMapping.ts exactly) ─
257
+ const CARD_SALE_ERROR_MAP = [
258
+ ['Insufficient Funds', 'Your card has insufficient funds. Please use a different payment method.'],
259
+ ['Invalid card number', 'The card number you entered is invalid. Please check and try again.'],
260
+ ['Card expired', 'Your card has expired. Please use a different card.'],
261
+ ['CVV Verification Failed', 'The CVV code you entered is incorrect. Please check and try again.'],
262
+ ['Address Verification Failed', 'The billing address does not match your card. Please verify your address.'],
263
+ ['Do Not Honor', 'Your card was declined. Please contact your bank or use a different payment method.'],
264
+ ['Declined', 'Your card was declined. Please contact your bank or use a different payment method.'],
265
+ ['Surcharge is currently not supported', 'Surcharge fees are not supported at this time.'],
266
+ ['Surcharge percent must be between', 'Surcharge fees are not supported at this time.'],
267
+ ['Forbidden - API key', 'Authentication failed. Please refresh the page.'],
268
+ ['Api Key is invalid', 'Authentication failed. Please refresh the page.'],
269
+ ['API key not found', 'Authentication failed. Please refresh the page.'],
270
+ ['Access token expired', 'Your session has expired. Please refresh the page.'],
271
+ ['Access token is invalid', 'Authentication failed. Please refresh the page.'],
272
+ ['Unauthorized', 'Authentication failed. Please refresh the page.'],
273
+ ['Too Many Requests', 'Too many requests. Please wait a moment and try again.'],
274
+ ['Rate limit exceeded', 'System is busy. Please wait a moment and try again.'],
275
+ ['No processor integrations found', 'Payment processing is not configured for this merchant. Please contact the merchant for assistance.'],
276
+ ['processor integration', 'Payment processing is temporarily unavailable. Please try again later or contact the merchant.'],
277
+ ['Invalid zipcode', 'The ZIP code you entered is invalid. Please check and try again.'],
278
+ ['failed to save to database', 'Your payment was processed but we encountered an issue. Please contact support.'],
279
+ ['CASHBACK UNAVAIL', 'This transaction type is not supported. Please try again or use a different payment method.'],
280
+ ];
281
+ /**
282
+ * Maps a raw Ozura Pay API cardSale error string to a user-facing message.
283
+ *
284
+ * Uses the exact same error key table as checkout's `getUserFriendlyError()` in
285
+ * errorMapping.ts so both surfaces produce identical copy for the same errors.
286
+ *
287
+ * Falls back to the original string when it's under 100 characters, or to a
288
+ * generic message for long/opaque server errors — matching checkout's fallback
289
+ * behaviour exactly.
290
+ *
291
+ * **Trade-off:** Short unrecognised strings (e.g. processor codes like
292
+ * `"PROC_TIMEOUT"`) are passed through verbatim. This intentionally mirrors
293
+ * checkout so the same raw Pay API errors produce the same user-facing text on
294
+ * both surfaces. If the Pay API ever returns internal codes that should never
295
+ * reach the UI, the fix belongs in the Pay API error normalisation layer rather
296
+ * than here.
297
+ */
298
+ function normalizeCardSaleError(raw) {
299
+ if (!raw)
300
+ return 'Payment processing failed. Please try again.';
301
+ for (const [key, message] of CARD_SALE_ERROR_MAP) {
302
+ if (raw.toLowerCase().includes(key.toLowerCase())) {
303
+ return message;
304
+ }
305
+ }
306
+ // Checkout fallback: pass through short errors, genericise long ones
307
+ if (raw.length < 100)
308
+ return raw;
309
+ return 'Payment processing failed. Please try again or contact support.';
310
+ }
311
+
312
312
  /**
313
313
  * Generates a RFC 4122 v4 UUID.
314
314
  *
@@ -896,6 +896,85 @@
896
896
  */
897
897
  const PROTOCOL_VERSION = 1;
898
898
 
899
+ /**
900
+ * Creates a `getSessionKey` callback for `OzVault.create()` and `<OzElements>`.
901
+ *
902
+ * This is the recommended way to wire the SDK to your backend session endpoint.
903
+ * If you don't need custom headers or auth logic, pass `sessionUrl` directly to
904
+ * `OzVault.create()` or `<OzElements>` — it calls this helper internally.
905
+ *
906
+ * The callback POSTs `{ sessionId }` to `url` and reads `sessionKey` (or the
907
+ * legacy `waxKey`) from the JSON response, so it is compatible with both the
908
+ * new `createSessionMiddleware` and the old `createMintWaxMiddleware` backends.
909
+ *
910
+ * Each call enforces a **10-second timeout**. On pure network failures
911
+ * (offline, DNS, connection refused) the request is retried **once after 750ms**.
912
+ * HTTP 4xx/5xx errors are never retried — they indicate misconfiguration.
913
+ *
914
+ * @param url - Absolute or relative URL of your session endpoint, e.g. `'/api/oz-session'`.
915
+ *
916
+ * @example
917
+ * // Simplest — just pass sessionUrl, no need to call this directly
918
+ * const vault = await OzVault.create({ pubKey: 'pk_live_...', sessionUrl: '/api/oz-session' });
919
+ *
920
+ * @example
921
+ * // Manual — use when you need custom headers
922
+ * const vault = await OzVault.create({
923
+ * pubKey: 'pk_live_...',
924
+ * getSessionKey: createSessionFetcher('/api/oz-session'),
925
+ * });
926
+ */
927
+ function createSessionFetcher(url) {
928
+ const TIMEOUT_MS = 10000;
929
+ // Each attempt gets its own AbortController so a timeout on attempt 1 does
930
+ // not bleed into the retry.
931
+ const attemptFetch = (sessionId) => {
932
+ const controller = new AbortController();
933
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
934
+ return fetch(url, {
935
+ method: 'POST',
936
+ headers: { 'Content-Type': 'application/json' },
937
+ body: JSON.stringify({ sessionId }),
938
+ signal: controller.signal,
939
+ }).finally(() => clearTimeout(timer));
940
+ };
941
+ return async (sessionId) => {
942
+ let res;
943
+ try {
944
+ res = await attemptFetch(sessionId);
945
+ }
946
+ catch (firstErr) {
947
+ // Timeout/abort — don't retry, we already waited the full duration.
948
+ if (firstErr instanceof Error && (firstErr.name === 'AbortError' || firstErr.name === 'TimeoutError')) {
949
+ throw new OzError(`Session endpoint timed out after ${TIMEOUT_MS / 1000}s (${url})`, undefined, 'timeout');
950
+ }
951
+ // Pure network error — retry once after a short pause.
952
+ await new Promise(resolve => setTimeout(resolve, 750));
953
+ try {
954
+ res = await attemptFetch(sessionId);
955
+ }
956
+ catch (retryErr) {
957
+ const msg = retryErr instanceof Error ? retryErr.message : 'Network error';
958
+ throw new OzError(`Could not reach session endpoint (${url}): ${msg}`, undefined, 'network');
959
+ }
960
+ }
961
+ const data = await res.json().catch(() => ({}));
962
+ if (!res.ok) {
963
+ throw new OzError(typeof data.error === 'string' && data.error
964
+ ? data.error
965
+ : `Session endpoint returned HTTP ${res.status}`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
966
+ }
967
+ // Accept both new `sessionKey` and legacy `waxKey` for backward compatibility
968
+ // with backends that haven't migrated to createSessionMiddleware yet.
969
+ const key = (typeof data.sessionKey === 'string' ? data.sessionKey : '') ||
970
+ (typeof data.waxKey === 'string' ? data.waxKey : '');
971
+ if (!key.trim()) {
972
+ throw new OzError('Session endpoint response is missing sessionKey. Check your /api/oz-session implementation.', undefined, 'validation');
973
+ }
974
+ return key;
975
+ };
976
+ }
977
+
899
978
  function isCardMetadata(v) {
900
979
  return !!v && typeof v === 'object' && typeof v.last4 === 'string';
901
980
  }
@@ -935,7 +1014,7 @@
935
1014
  * @internal
936
1015
  */
937
1016
  constructor(options, waxKey, tokenizationSessionId) {
938
- var _a, _b, _c, _d;
1017
+ var _a, _b, _c, _d, _e, _f;
939
1018
  this.elements = new Map();
940
1019
  this.elementsByType = new Map();
941
1020
  this.bankElementsByType = new Map();
@@ -969,15 +1048,16 @@
969
1048
  this.fonts = (_a = options.fonts) !== null && _a !== void 0 ? _a : [];
970
1049
  this.resolvedAppearance = resolveAppearance(options.appearance);
971
1050
  this.vaultId = `vault-${uuid()}`;
972
- this._maxTokenizeCalls = (_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3;
973
- this._debug = (_c = options.debug) !== null && _c !== void 0 ? _c : false;
1051
+ // sessionLimit takes precedence over legacy maxTokenizeCalls
1052
+ this._maxTokenizeCalls = (_c = (_b = options.sessionLimit) !== null && _b !== void 0 ? _b : options.maxTokenizeCalls) !== null && _c !== void 0 ? _c : 3;
1053
+ this._debug = (_d = options.debug) !== null && _d !== void 0 ? _d : false;
974
1054
  this.boundHandleMessage = this.handleMessage.bind(this);
975
1055
  window.addEventListener('message', this.boundHandleMessage);
976
1056
  this.boundHandleVisibility = this.handleVisibilityChange.bind(this);
977
1057
  document.addEventListener('visibilitychange', this.boundHandleVisibility);
978
1058
  this.mountTokenizerFrame();
979
1059
  if (options.onLoadError) {
980
- const timeout = (_d = options.loadTimeoutMs) !== null && _d !== void 0 ? _d : 10000;
1060
+ const timeout = (_e = options.loadTimeoutMs) !== null && _e !== void 0 ? _e : 10000;
981
1061
  this.loadErrorTimeoutId = setTimeout(() => {
982
1062
  this.loadErrorTimeoutId = null;
983
1063
  if (!this._destroyed && !this.tokenizerReady) {
@@ -985,7 +1065,8 @@
985
1065
  }
986
1066
  }, timeout);
987
1067
  }
988
- this._onWaxRefresh = options.onWaxRefresh;
1068
+ // onSessionRefresh takes precedence over legacy onWaxRefresh
1069
+ this._onWaxRefresh = (_f = options.onSessionRefresh) !== null && _f !== void 0 ? _f : options.onWaxRefresh;
989
1070
  this._onReady = options.onReady;
990
1071
  this.log('vault created', { vaultId: this.vaultId, frameBaseUrl: this.frameBaseUrl, maxTokenizeCalls: this._maxTokenizeCalls });
991
1072
  }
@@ -993,47 +1074,59 @@
993
1074
  * Creates and returns a ready `OzVault` instance.
994
1075
  *
995
1076
  * Internally this:
996
- * 1. Generates a `tokenizationSessionId` (UUID).
1077
+ * 1. Generates a session UUID.
997
1078
  * 2. Starts loading the hidden tokenizer iframe immediately.
998
- * 3. Calls `options.fetchWaxKey(tokenizationSessionId)` concurrently — your
999
- * backend mints a session-bound wax key from the vault and returns it.
1000
- * 4. Resolves with the vault instance once the wax key is stored. The iframe
1079
+ * 3. Fetches a session key from your backend concurrently — either via
1080
+ * `sessionUrl` (simplest), `getSessionKey` (custom headers/auth), or the
1081
+ * deprecated `fetchWaxKey` callback.
1082
+ * 4. Resolves with the vault instance once the session key is stored. The iframe
1001
1083
  * has been loading the whole time, so `isReady` may already be true or
1002
1084
  * will fire shortly after.
1003
1085
  *
1004
1086
  * The returned vault is ready to create elements immediately. `createToken()`
1005
1087
  * additionally requires `vault.isReady` (tokenizer iframe loaded).
1006
1088
  *
1007
- * @throws {OzError} if `fetchWaxKey` throws, returns a non-string value, or returns an empty/whitespace-only string.
1089
+ * @throws {OzError} if the session fetch fails, times out, or returns an empty string.
1008
1090
  */
1009
1091
  static async create(options, signal) {
1010
1092
  if (!options.pubKey || !options.pubKey.trim()) {
1011
1093
  throw new OzError('pubKey is required in options. Obtain your public key from the Ozura admin.');
1012
1094
  }
1013
- if (typeof options.fetchWaxKey !== 'function') {
1014
- throw new OzError('fetchWaxKey must be a function. See OzVault.create() docs for the expected signature.');
1095
+ // Normalize the session callback. Priority: sessionUrl > getSessionKey > fetchWaxKey (deprecated).
1096
+ // This allows merchants to use the clean new API without touching legacy code.
1097
+ let resolvedFetchKey;
1098
+ if (options.sessionUrl) {
1099
+ resolvedFetchKey = createSessionFetcher(options.sessionUrl);
1100
+ }
1101
+ else if (typeof options.getSessionKey === 'function') {
1102
+ resolvedFetchKey = options.getSessionKey;
1103
+ }
1104
+ else if (typeof options.fetchWaxKey === 'function') {
1105
+ resolvedFetchKey = options.fetchWaxKey;
1106
+ }
1107
+ else {
1108
+ throw new OzError('A session URL or callback is required. Pass sessionUrl, getSessionKey, or fetchWaxKey to OzVault.create().');
1015
1109
  }
1016
1110
  const tokenizationSessionId = uuid();
1017
1111
  // Construct the vault immediately — this mounts the tokenizer iframe so it
1018
- // starts loading while fetchWaxKey is in flight. The waxKey field starts
1019
- // empty and is set below before create() returns.
1112
+ // starts loading while the session fetch is in flight.
1020
1113
  const vault = new OzVault(options, '', tokenizationSessionId);
1021
1114
  // If the caller provides an AbortSignal (e.g. React useEffect cleanup),
1022
1115
  // destroy the vault immediately on abort so the tokenizer iframe and message
1023
- // listener are removed synchronously rather than waiting for fetchWaxKey to
1024
- // settle. This eliminates the brief double-iframe window in React StrictMode.
1116
+ // listener are removed synchronously rather than waiting for the session fetch
1117
+ // to settle. This eliminates the brief double-iframe window in React StrictMode.
1025
1118
  const onAbort = () => vault.destroy();
1026
1119
  signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onAbort, { once: true });
1027
1120
  let waxKey;
1028
1121
  try {
1029
- waxKey = await options.fetchWaxKey(tokenizationSessionId);
1122
+ waxKey = await resolvedFetchKey(tokenizationSessionId);
1030
1123
  }
1031
1124
  catch (err) {
1032
1125
  signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
1033
1126
  vault.destroy();
1034
1127
  if (signal === null || signal === void 0 ? void 0 : signal.aborted)
1035
1128
  throw new OzError('OzVault.create() was cancelled.');
1036
- // Preserve errorCode/retryable from OzError (e.g. timeout/network from createFetchWaxKey)
1129
+ // Preserve errorCode/retryable from OzError (e.g. timeout/network from createSessionFetcher)
1037
1130
  // so callers can distinguish transient failures from config errors.
1038
1131
  const originalCode = err instanceof OzError ? err.errorCode : undefined;
1039
1132
  const msg = err instanceof Error ? err.message : 'Unknown error';
@@ -1050,7 +1143,7 @@
1050
1143
  }
1051
1144
  // Static methods can access private fields of instances of the same class.
1052
1145
  vault.waxKey = waxKey;
1053
- vault._storedFetchWaxKey = options.fetchWaxKey;
1146
+ vault._storedFetchWaxKey = resolvedFetchKey;
1054
1147
  // If the tokenizer iframe fired OZ_FRAME_READY before fetchWaxKey resolved,
1055
1148
  // the OZ_INIT sent at that point had an empty waxKey. Send a follow-up now
1056
1149
  // so the tokenizer has the key stored before any createToken() call.
@@ -1964,88 +2057,11 @@
1964
2057
  }
1965
2058
  }
1966
2059
 
1967
- /**
1968
- * Creates a ready-to-use `fetchWaxKey` callback for `OzVault.create()` and `<OzElements>`.
1969
- *
1970
- * Calls your backend mint endpoint with `{ sessionId }` and returns the wax key string.
1971
- * Throws on non-OK responses or a missing `waxKey` field so the vault can surface the
1972
- * error through its normal error path.
1973
- *
1974
- * Each call enforces a 10-second per-attempt timeout. On a pure network-level
1975
- * failure (connection refused, DNS failure, etc.) the call is retried once after
1976
- * 750ms before throwing. HTTP errors (4xx/5xx) are never retried — they indicate
1977
- * an endpoint misconfiguration or an invalid key, not a transient failure.
1978
- *
1979
- * The mint endpoint is typically the one-line `createMintWaxHandler` / `createMintWaxMiddleware`
1980
- * from `@ozura/elements/server`.
1981
- *
1982
- * @param mintUrl - Absolute or relative URL of your wax-key mint endpoint, e.g. `'/api/mint-wax'`.
1983
- *
1984
- * @example
1985
- * // Vanilla JS
1986
- * const vault = await OzVault.create({
1987
- * pubKey: 'pk_live_...',
1988
- * fetchWaxKey: createFetchWaxKey('/api/mint-wax'),
1989
- * });
1990
- *
1991
- * @example
1992
- * // React
1993
- * <OzElements pubKey="pk_live_..." fetchWaxKey={createFetchWaxKey('/api/mint-wax')}>
1994
- */
1995
- function createFetchWaxKey(mintUrl) {
1996
- const TIMEOUT_MS = 10000;
1997
- // Each attempt gets its own AbortController so a timeout on attempt 1 does
1998
- // not bleed into the retry. Uses AbortController + setTimeout instead of
1999
- // AbortSignal.timeout() to support environments without that API.
2000
- const attemptFetch = (sessionId) => {
2001
- const controller = new AbortController();
2002
- const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
2003
- return fetch(mintUrl, {
2004
- method: 'POST',
2005
- headers: { 'Content-Type': 'application/json' },
2006
- body: JSON.stringify({ sessionId }),
2007
- signal: controller.signal,
2008
- }).finally(() => clearTimeout(timer));
2009
- };
2010
- return async (sessionId) => {
2011
- let res;
2012
- try {
2013
- res = await attemptFetch(sessionId);
2014
- }
2015
- catch (firstErr) {
2016
- // Abort/timeout should not be retried — the server received nothing or
2017
- // we already waited the full timeout duration.
2018
- if (firstErr instanceof Error && (firstErr.name === 'AbortError' || firstErr.name === 'TimeoutError')) {
2019
- throw new OzError(`Wax key mint timed out after ${TIMEOUT_MS / 1000}s (${mintUrl})`, undefined, 'timeout');
2020
- }
2021
- // Pure network error (offline, DNS, connection refused) — retry once
2022
- // after a short pause in case of a transient blip.
2023
- await new Promise(resolve => setTimeout(resolve, 750));
2024
- try {
2025
- res = await attemptFetch(sessionId);
2026
- }
2027
- catch (retryErr) {
2028
- const msg = retryErr instanceof Error ? retryErr.message : 'Network error';
2029
- throw new OzError(`Could not reach wax key mint endpoint (${mintUrl}): ${msg}`, undefined, 'network');
2030
- }
2031
- }
2032
- const data = await res.json().catch(() => ({}));
2033
- if (!res.ok) {
2034
- throw new OzError(typeof data.error === 'string' && data.error
2035
- ? data.error
2036
- : `Wax key mint failed (HTTP ${res.status})`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
2037
- }
2038
- if (typeof data.waxKey !== 'string' || !data.waxKey.trim()) {
2039
- throw new OzError('Mint endpoint response is missing waxKey. Check your /api/mint-wax implementation.', undefined, 'validation');
2040
- }
2041
- return data.waxKey;
2042
- };
2043
- }
2044
-
2045
2060
  exports.OzElement = OzElement;
2046
2061
  exports.OzError = OzError;
2047
2062
  exports.OzVault = OzVault;
2048
- exports.createFetchWaxKey = createFetchWaxKey;
2063
+ exports.createFetchWaxKey = createSessionFetcher;
2064
+ exports.createSessionFetcher = createSessionFetcher;
2049
2065
  exports.normalizeBankVaultError = normalizeBankVaultError;
2050
2066
  exports.normalizeCardSaleError = normalizeCardSaleError;
2051
2067
  exports.normalizeVaultError = normalizeVaultError;