@ozura/elements 1.0.2 → 1.1.0-next.22

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 (42) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +234 -86
  3. package/dist/frame/element-frame.js +92 -20
  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 +20 -4
  7. package/dist/frame/tokenizer-frame.js.map +1 -1
  8. package/dist/oz-elements.esm.js +477 -251
  9. package/dist/oz-elements.esm.js.map +1 -1
  10. package/dist/oz-elements.umd.js +478 -251
  11. package/dist/oz-elements.umd.js.map +1 -1
  12. package/dist/react/frame/elementFrame.d.ts +70 -1
  13. package/dist/react/frame/protocol.d.ts +12 -0
  14. package/dist/react/index.cjs.js +447 -225
  15. package/dist/react/index.cjs.js.map +1 -1
  16. package/dist/react/index.esm.js +448 -226
  17. package/dist/react/index.esm.js.map +1 -1
  18. package/dist/react/react/index.d.ts +70 -26
  19. package/dist/react/sdk/OzVault.d.ts +55 -5
  20. package/dist/react/sdk/createSessionFetcher.d.ts +29 -0
  21. package/dist/react/sdk/index.d.ts +6 -26
  22. package/dist/react/server/index.d.ts +126 -74
  23. package/dist/react/types/index.d.ts +72 -31
  24. package/dist/server/frame/elementFrame.d.ts +70 -1
  25. package/dist/server/frame/protocol.d.ts +12 -0
  26. package/dist/server/index.cjs.js +167 -78
  27. package/dist/server/index.cjs.js.map +1 -1
  28. package/dist/server/index.esm.js +166 -79
  29. package/dist/server/index.esm.js.map +1 -1
  30. package/dist/server/sdk/OzVault.d.ts +55 -5
  31. package/dist/server/sdk/createSessionFetcher.d.ts +29 -0
  32. package/dist/server/sdk/index.d.ts +6 -26
  33. package/dist/server/server/index.d.ts +126 -74
  34. package/dist/server/types/index.d.ts +72 -31
  35. package/dist/types/frame/elementFrame.d.ts +70 -1
  36. package/dist/types/frame/protocol.d.ts +12 -0
  37. package/dist/types/sdk/OzVault.d.ts +55 -5
  38. package/dist/types/sdk/createSessionFetcher.d.ts +29 -0
  39. package/dist/types/sdk/index.d.ts +6 -26
  40. package/dist/types/server/index.d.ts +126 -74
  41. package/dist/types/types/index.d.ts +72 -31
  42. package/package.json +1 -1
@@ -1,144 +1,3 @@
1
- /**
2
- * errors.ts — error types and normalisation for OzElements.
3
- *
4
- * Three normalisation functions:
5
- * - normalizeVaultError — maps raw vault /tokenize errors to user-facing messages (card flows)
6
- * - normalizeBankVaultError — maps raw vault /tokenize errors to user-facing messages (bank/ACH flows)
7
- * - normalizeCardSaleError — maps raw cardSale API errors to user-facing messages
8
- *
9
- * Error keys in normalizeCardSaleError are taken directly from checkout's
10
- * errorMapping.ts so the same error strings produce the same user-facing copy.
11
- */
12
- const OZ_ERROR_CODES = new Set(['network', 'timeout', 'auth', 'validation', 'server', 'config', 'unknown']);
13
- /** Returns true and narrows to OzErrorCode when `value` is a valid member of the union. */
14
- function isOzErrorCode(value) {
15
- return typeof value === 'string' && OZ_ERROR_CODES.has(value);
16
- }
17
- class OzError extends Error {
18
- constructor(message, raw, errorCode) {
19
- super(message);
20
- this.name = 'OzError';
21
- this.raw = raw !== null && raw !== void 0 ? raw : message;
22
- this.errorCode = errorCode !== null && errorCode !== void 0 ? errorCode : 'unknown';
23
- this.retryable = this.errorCode === 'network' || this.errorCode === 'timeout' || this.errorCode === 'server';
24
- }
25
- }
26
- /** Shared patterns that apply to both card and bank vault errors. */
27
- function normalizeCommonVaultError(msg) {
28
- if (msg.includes('api key') || msg.includes('unauthorized') || msg.includes('authentication') || msg.includes('forbidden')) {
29
- return 'Authentication failed. Check your vault API key configuration.';
30
- }
31
- if (msg.includes('network') || msg.includes('failed to fetch') || msg.includes('networkerror')) {
32
- return 'A network error occurred. Please check your connection and try again.';
33
- }
34
- if (msg.includes('timeout') || msg.includes('timed out')) {
35
- return 'The request timed out. Please try again.';
36
- }
37
- if (msg.includes('http 5') || msg.includes('500') || msg.includes('502') || msg.includes('503')) {
38
- return 'A server error occurred. Please try again shortly.';
39
- }
40
- return null;
41
- }
42
- /**
43
- * Maps a raw vault /tokenize error string to a user-facing message for card flows.
44
- * Falls back to the original string if no pattern matches.
45
- */
46
- function normalizeVaultError(raw) {
47
- const msg = raw.toLowerCase();
48
- if (msg.includes('card number') || msg.includes('invalid card') || msg.includes('luhn')) {
49
- return 'The card number is invalid. Please check and try again.';
50
- }
51
- if (msg.includes('expir')) {
52
- return 'The card expiration date is invalid or the card has expired.';
53
- }
54
- if (msg.includes('cvv') || msg.includes('cvc') || msg.includes('security code')) {
55
- return 'The CVV code is invalid. Please check and try again.';
56
- }
57
- if (msg.includes('insufficient') || msg.includes('funds')) {
58
- return 'Your card has insufficient funds. Please use a different card.';
59
- }
60
- if (msg.includes('declined') || msg.includes('do not honor')) {
61
- return 'Your card was declined. Please try a different card or contact your bank.';
62
- }
63
- const common = normalizeCommonVaultError(msg);
64
- if (common)
65
- return common;
66
- return raw;
67
- }
68
- /**
69
- * Maps a raw vault /tokenize error string to a user-facing message for bank (ACH) flows.
70
- * Uses bank-specific pattern matching so card-specific messages are never shown for
71
- * bank errors. Falls back to the original string if no pattern matches.
72
- */
73
- function normalizeBankVaultError(raw) {
74
- const msg = raw.toLowerCase();
75
- if (msg.includes('account number') || msg.includes('account_number') || msg.includes('invalid account')) {
76
- return 'The bank account number is invalid. Please check and try again.';
77
- }
78
- if (msg.includes('routing number') || msg.includes('routing_number') || msg.includes('invalid routing') || /\baba\b/.test(msg)) {
79
- return 'The routing number is invalid. Please check and try again.';
80
- }
81
- const common = normalizeCommonVaultError(msg);
82
- if (common)
83
- return common;
84
- return raw;
85
- }
86
- // ─── cardSale error map (mirrors checkout/src/utils/errorMapping.ts exactly) ─
87
- const CARD_SALE_ERROR_MAP = [
88
- ['Insufficient Funds', 'Your card has insufficient funds. Please use a different payment method.'],
89
- ['Invalid card number', 'The card number you entered is invalid. Please check and try again.'],
90
- ['Card expired', 'Your card has expired. Please use a different card.'],
91
- ['CVV Verification Failed', 'The CVV code you entered is incorrect. Please check and try again.'],
92
- ['Address Verification Failed', 'The billing address does not match your card. Please verify your address.'],
93
- ['Do Not Honor', 'Your card was declined. Please contact your bank or use a different payment method.'],
94
- ['Declined', 'Your card was declined. Please contact your bank or use a different payment method.'],
95
- ['Surcharge is currently not supported', 'Surcharge fees are not supported at this time.'],
96
- ['Surcharge percent must be between', 'Surcharge fees are not supported at this time.'],
97
- ['Forbidden - API key', 'Authentication failed. Please refresh the page.'],
98
- ['Api Key is invalid', 'Authentication failed. Please refresh the page.'],
99
- ['API key not found', 'Authentication failed. Please refresh the page.'],
100
- ['Access token expired', 'Your session has expired. Please refresh the page.'],
101
- ['Access token is invalid', 'Authentication failed. Please refresh the page.'],
102
- ['Unauthorized', 'Authentication failed. Please refresh the page.'],
103
- ['Too Many Requests', 'Too many requests. Please wait a moment and try again.'],
104
- ['Rate limit exceeded', 'System is busy. Please wait a moment and try again.'],
105
- ['No processor integrations found', 'Payment processing is not configured for this merchant. Please contact the merchant for assistance.'],
106
- ['processor integration', 'Payment processing is temporarily unavailable. Please try again later or contact the merchant.'],
107
- ['Invalid zipcode', 'The ZIP code you entered is invalid. Please check and try again.'],
108
- ['failed to save to database', 'Your payment was processed but we encountered an issue. Please contact support.'],
109
- ['CASHBACK UNAVAIL', 'This transaction type is not supported. Please try again or use a different payment method.'],
110
- ];
111
- /**
112
- * Maps a raw Ozura Pay API cardSale error string to a user-facing message.
113
- *
114
- * Uses the exact same error key table as checkout's `getUserFriendlyError()` in
115
- * errorMapping.ts so both surfaces produce identical copy for the same errors.
116
- *
117
- * Falls back to the original string when it's under 100 characters, or to a
118
- * generic message for long/opaque server errors — matching checkout's fallback
119
- * behaviour exactly.
120
- *
121
- * **Trade-off:** Short unrecognised strings (e.g. processor codes like
122
- * `"PROC_TIMEOUT"`) are passed through verbatim. This intentionally mirrors
123
- * checkout so the same raw Pay API errors produce the same user-facing text on
124
- * both surfaces. If the Pay API ever returns internal codes that should never
125
- * reach the UI, the fix belongs in the Pay API error normalisation layer rather
126
- * than here.
127
- */
128
- function normalizeCardSaleError(raw) {
129
- if (!raw)
130
- return 'Payment processing failed. Please try again.';
131
- for (const [key, message] of CARD_SALE_ERROR_MAP) {
132
- if (raw.toLowerCase().includes(key.toLowerCase())) {
133
- return message;
134
- }
135
- }
136
- // Checkout fallback: pass through short errors, genericise long ones
137
- if (raw.length < 100)
138
- return raw;
139
- return 'Payment processing failed. Please try again or contact support.';
140
- }
141
-
142
1
  const THEME_DEFAULT = {
143
2
  base: {
144
3
  color: '#1a1a2e',
@@ -303,6 +162,147 @@ function mergeAppearanceWithElementStyle(appearance, elementStyle) {
303
162
  return mergeStyleConfigs(appearance, elementStyle);
304
163
  }
305
164
 
165
+ /**
166
+ * errors.ts — error types and normalisation for OzElements.
167
+ *
168
+ * Three normalisation functions:
169
+ * - normalizeVaultError — maps raw vault /tokenize errors to user-facing messages (card flows)
170
+ * - normalizeBankVaultError — maps raw vault /tokenize errors to user-facing messages (bank/ACH flows)
171
+ * - normalizeCardSaleError — maps raw cardSale API errors to user-facing messages
172
+ *
173
+ * Error keys in normalizeCardSaleError are taken directly from checkout's
174
+ * errorMapping.ts so the same error strings produce the same user-facing copy.
175
+ */
176
+ const OZ_ERROR_CODES = new Set(['network', 'timeout', 'auth', 'validation', 'server', 'config', 'unknown']);
177
+ /** Returns true and narrows to OzErrorCode when `value` is a valid member of the union. */
178
+ function isOzErrorCode(value) {
179
+ return typeof value === 'string' && OZ_ERROR_CODES.has(value);
180
+ }
181
+ class OzError extends Error {
182
+ constructor(message, raw, errorCode) {
183
+ super(message);
184
+ this.name = 'OzError';
185
+ this.raw = raw !== null && raw !== void 0 ? raw : message;
186
+ this.errorCode = errorCode !== null && errorCode !== void 0 ? errorCode : 'unknown';
187
+ this.retryable = this.errorCode === 'network' || this.errorCode === 'timeout' || this.errorCode === 'server';
188
+ }
189
+ }
190
+ /** Shared patterns that apply to both card and bank vault errors. */
191
+ function normalizeCommonVaultError(msg) {
192
+ if (msg.includes('api key') || msg.includes('unauthorized') || msg.includes('authentication') || msg.includes('forbidden')) {
193
+ return 'Authentication failed. Check your vault API key configuration.';
194
+ }
195
+ if (msg.includes('network') || msg.includes('failed to fetch') || msg.includes('networkerror')) {
196
+ return 'A network error occurred. Please check your connection and try again.';
197
+ }
198
+ if (msg.includes('timeout') || msg.includes('timed out')) {
199
+ return 'The request timed out. Please try again.';
200
+ }
201
+ if (msg.includes('http 5') || msg.includes('500') || msg.includes('502') || msg.includes('503')) {
202
+ return 'A server error occurred. Please try again shortly.';
203
+ }
204
+ return null;
205
+ }
206
+ /**
207
+ * Maps a raw vault /tokenize error string to a user-facing message for card flows.
208
+ * Falls back to the original string if no pattern matches.
209
+ */
210
+ function normalizeVaultError(raw) {
211
+ const msg = raw.toLowerCase();
212
+ if (msg.includes('card number') || msg.includes('invalid card') || msg.includes('luhn')) {
213
+ return 'The card number is invalid. Please check and try again.';
214
+ }
215
+ if (msg.includes('expir')) {
216
+ return 'The card expiration date is invalid or the card has expired.';
217
+ }
218
+ if (msg.includes('cvv') || msg.includes('cvc') || msg.includes('security code')) {
219
+ return 'The CVV code is invalid. Please check and try again.';
220
+ }
221
+ if (msg.includes('insufficient') || msg.includes('funds')) {
222
+ return 'Your card has insufficient funds. Please use a different card.';
223
+ }
224
+ if (msg.includes('declined') || msg.includes('do not honor')) {
225
+ return 'Your card was declined. Please try a different card or contact your bank.';
226
+ }
227
+ const common = normalizeCommonVaultError(msg);
228
+ if (common)
229
+ return common;
230
+ return raw;
231
+ }
232
+ /**
233
+ * Maps a raw vault /tokenize error string to a user-facing message for bank (ACH) flows.
234
+ * Uses bank-specific pattern matching so card-specific messages are never shown for
235
+ * bank errors. Falls back to the original string if no pattern matches.
236
+ */
237
+ function normalizeBankVaultError(raw) {
238
+ const msg = raw.toLowerCase();
239
+ if (msg.includes('account number') || msg.includes('account_number') || msg.includes('invalid account')) {
240
+ return 'The bank account number is invalid. Please check and try again.';
241
+ }
242
+ if (msg.includes('routing number') || msg.includes('routing_number') || msg.includes('invalid routing') || /\baba\b/.test(msg)) {
243
+ return 'The routing number is invalid. Please check and try again.';
244
+ }
245
+ const common = normalizeCommonVaultError(msg);
246
+ if (common)
247
+ return common;
248
+ return raw;
249
+ }
250
+ // ─── cardSale error map (mirrors checkout/src/utils/errorMapping.ts exactly) ─
251
+ const CARD_SALE_ERROR_MAP = [
252
+ ['Insufficient Funds', 'Your card has insufficient funds. Please use a different payment method.'],
253
+ ['Invalid card number', 'The card number you entered is invalid. Please check and try again.'],
254
+ ['Card expired', 'Your card has expired. Please use a different card.'],
255
+ ['CVV Verification Failed', 'The CVV code you entered is incorrect. Please check and try again.'],
256
+ ['Address Verification Failed', 'The billing address does not match your card. Please verify your address.'],
257
+ ['Do Not Honor', 'Your card was declined. Please contact your bank or use a different payment method.'],
258
+ ['Declined', 'Your card was declined. Please contact your bank or use a different payment method.'],
259
+ ['Surcharge is currently not supported', 'Surcharge fees are not supported at this time.'],
260
+ ['Surcharge percent must be between', 'Surcharge fees are not supported at this time.'],
261
+ ['Forbidden - API key', 'Authentication failed. Please refresh the page.'],
262
+ ['Api Key is invalid', 'Authentication failed. Please refresh the page.'],
263
+ ['API key not found', 'Authentication failed. Please refresh the page.'],
264
+ ['Access token expired', 'Your session has expired. Please refresh the page.'],
265
+ ['Access token is invalid', 'Authentication failed. Please refresh the page.'],
266
+ ['Unauthorized', 'Authentication failed. Please refresh the page.'],
267
+ ['Too Many Requests', 'Too many requests. Please wait a moment and try again.'],
268
+ ['Rate limit exceeded', 'System is busy. Please wait a moment and try again.'],
269
+ ['No processor integrations found', 'Payment processing is not configured for this merchant. Please contact the merchant for assistance.'],
270
+ ['processor integration', 'Payment processing is temporarily unavailable. Please try again later or contact the merchant.'],
271
+ ['Invalid zipcode', 'The ZIP code you entered is invalid. Please check and try again.'],
272
+ ['failed to save to database', 'Your payment was processed but we encountered an issue. Please contact support.'],
273
+ ['CASHBACK UNAVAIL', 'This transaction type is not supported. Please try again or use a different payment method.'],
274
+ ];
275
+ /**
276
+ * Maps a raw Ozura Pay API cardSale error string to a user-facing message.
277
+ *
278
+ * Uses the exact same error key table as checkout's `getUserFriendlyError()` in
279
+ * errorMapping.ts so both surfaces produce identical copy for the same errors.
280
+ *
281
+ * Falls back to the original string when it's under 100 characters, or to a
282
+ * generic message for long/opaque server errors — matching checkout's fallback
283
+ * behaviour exactly.
284
+ *
285
+ * **Trade-off:** Short unrecognised strings (e.g. processor codes like
286
+ * `"PROC_TIMEOUT"`) are passed through verbatim. This intentionally mirrors
287
+ * checkout so the same raw Pay API errors produce the same user-facing text on
288
+ * both surfaces. If the Pay API ever returns internal codes that should never
289
+ * reach the UI, the fix belongs in the Pay API error normalisation layer rather
290
+ * than here.
291
+ */
292
+ function normalizeCardSaleError(raw) {
293
+ if (!raw)
294
+ return 'Payment processing failed. Please try again.';
295
+ for (const [key, message] of CARD_SALE_ERROR_MAP) {
296
+ if (raw.toLowerCase().includes(key.toLowerCase())) {
297
+ return message;
298
+ }
299
+ }
300
+ // Checkout fallback: pass through short errors, genericise long ones
301
+ if (raw.length < 100)
302
+ return raw;
303
+ return 'Payment processing failed. Please try again or contact support.';
304
+ }
305
+
306
306
  /**
307
307
  * Generates a RFC 4122 v4 UUID.
308
308
  *
@@ -407,7 +407,7 @@ class OzElement {
407
407
  * (useful when integrating with React refs).
408
408
  */
409
409
  mount(target) {
410
- var _a;
410
+ var _a, _b;
411
411
  if (this._destroyed)
412
412
  throw new OzError('Cannot mount a destroyed element.');
413
413
  if (this.iframe)
@@ -424,7 +424,13 @@ class OzElement {
424
424
  iframe.setAttribute('scrolling', 'no');
425
425
  iframe.setAttribute('allowtransparency', 'true');
426
426
  iframe.style.cssText = 'border:none;width:100%;height:46px;display:block;overflow:hidden;';
427
- iframe.title = `Secure ${this.elementType} input`;
427
+ iframe.title = `Secure ${(_a = {
428
+ cardNumber: 'card number',
429
+ expirationDate: 'expiration date',
430
+ cvv: 'CVV',
431
+ accountNumber: 'account number',
432
+ routingNumber: 'routing number',
433
+ }[this.elementType]) !== null && _a !== void 0 ? _a : this.elementType} input`;
428
434
  // Note: the `sandbox` attribute is intentionally NOT set. Field values are
429
435
  // delivered to the tokenizer iframe via a MessageChannel port (transferred
430
436
  // in OZ_BEGIN_COLLECT), so no window.parent named-frame lookup is needed.
@@ -438,7 +444,7 @@ class OzElement {
438
444
  container.appendChild(iframe);
439
445
  this.iframe = iframe;
440
446
  this._frameWindow = iframe.contentWindow;
441
- const timeout = (_a = this.options.loadTimeoutMs) !== null && _a !== void 0 ? _a : 10000;
447
+ const timeout = (_b = this.options.loadTimeoutMs) !== null && _b !== void 0 ? _b : 10000;
442
448
  this._loadTimer = setTimeout(() => {
443
449
  if (!this._ready && !this._destroyed) {
444
450
  this.emit('loaderror', { elementType: this.elementType, error: `${this.elementType} iframe failed to load within ${timeout}ms` });
@@ -871,13 +877,105 @@ function validateBilling(billing) {
871
877
  return { valid: errors.length === 0, errors, normalized };
872
878
  }
873
879
 
880
+ /**
881
+ * Shared postMessage protocol constants.
882
+ *
883
+ * PROTOCOL_VERSION must be incremented any time a breaking change is made to
884
+ * the postMessage message shape (new required fields, renamed types, removed
885
+ * fields, changed semantics). The SDK reads this value from OZ_FRAME_READY
886
+ * messages and warns when the frame and SDK are out of sync.
887
+ *
888
+ * Non-breaking additions (new optional fields, new message types that old
889
+ * frames can safely ignore) do NOT require a version bump.
890
+ */
891
+ const PROTOCOL_VERSION = 1;
892
+
893
+ /**
894
+ * Creates a `getSessionKey` callback for `OzVault.create()` and `<OzElements>`.
895
+ *
896
+ * This is the recommended way to wire the SDK to your backend session endpoint.
897
+ * If you don't need custom headers or auth logic, pass `sessionUrl` directly to
898
+ * `OzVault.create()` or `<OzElements>` — it calls this helper internally.
899
+ *
900
+ * The callback POSTs `{ sessionId }` to `url` and reads `sessionKey` (or the
901
+ * legacy `waxKey`) from the JSON response, so it is compatible with both the
902
+ * new `createSessionMiddleware` and the old `createMintWaxMiddleware` backends.
903
+ *
904
+ * Each call enforces a **10-second timeout**. On pure network failures
905
+ * (offline, DNS, connection refused) the request is retried **once after 750ms**.
906
+ * HTTP 4xx/5xx errors are never retried — they indicate misconfiguration.
907
+ *
908
+ * @param url - Absolute or relative URL of your session endpoint, e.g. `'/api/oz-session'`.
909
+ *
910
+ * @example
911
+ * // Simplest — just pass sessionUrl, no need to call this directly
912
+ * const vault = await OzVault.create({ pubKey: 'pk_live_...', sessionUrl: '/api/oz-session' });
913
+ *
914
+ * @example
915
+ * // Manual — use when you need custom headers
916
+ * const vault = await OzVault.create({
917
+ * pubKey: 'pk_live_...',
918
+ * getSessionKey: createSessionFetcher('/api/oz-session'),
919
+ * });
920
+ */
921
+ function createSessionFetcher(url) {
922
+ const TIMEOUT_MS = 10000;
923
+ // Each attempt gets its own AbortController so a timeout on attempt 1 does
924
+ // not bleed into the retry.
925
+ const attemptFetch = (sessionId) => {
926
+ const controller = new AbortController();
927
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
928
+ return fetch(url, {
929
+ method: 'POST',
930
+ headers: { 'Content-Type': 'application/json' },
931
+ body: JSON.stringify({ sessionId }),
932
+ signal: controller.signal,
933
+ }).finally(() => clearTimeout(timer));
934
+ };
935
+ return async (sessionId) => {
936
+ let res;
937
+ try {
938
+ res = await attemptFetch(sessionId);
939
+ }
940
+ catch (firstErr) {
941
+ // Timeout/abort — don't retry, we already waited the full duration.
942
+ if (firstErr instanceof Error && (firstErr.name === 'AbortError' || firstErr.name === 'TimeoutError')) {
943
+ throw new OzError(`Session endpoint timed out after ${TIMEOUT_MS / 1000}s (${url})`, undefined, 'timeout');
944
+ }
945
+ // Pure network error — retry once after a short pause.
946
+ await new Promise(resolve => setTimeout(resolve, 750));
947
+ try {
948
+ res = await attemptFetch(sessionId);
949
+ }
950
+ catch (retryErr) {
951
+ const msg = retryErr instanceof Error ? retryErr.message : 'Network error';
952
+ throw new OzError(`Could not reach session endpoint (${url}): ${msg}`, undefined, 'network');
953
+ }
954
+ }
955
+ const data = await res.json().catch(() => ({}));
956
+ if (!res.ok) {
957
+ throw new OzError(typeof data.error === 'string' && data.error
958
+ ? data.error
959
+ : `Session endpoint returned HTTP ${res.status}`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
960
+ }
961
+ // Accept both new `sessionKey` and legacy `waxKey` for backward compatibility
962
+ // with backends that haven't migrated to createSessionMiddleware yet.
963
+ const key = (typeof data.sessionKey === 'string' ? data.sessionKey : '') ||
964
+ (typeof data.waxKey === 'string' ? data.waxKey : '');
965
+ if (!key.trim()) {
966
+ throw new OzError('Session endpoint response is missing sessionKey. Check your /api/oz-session implementation.', undefined, 'validation');
967
+ }
968
+ return key;
969
+ };
970
+ }
971
+
874
972
  function isCardMetadata(v) {
875
973
  return !!v && typeof v === 'object' && typeof v.last4 === 'string';
876
974
  }
877
975
  function isBankAccountMetadata(v) {
878
976
  return !!v && typeof v === 'object' && typeof v.last4 === 'string';
879
977
  }
880
- const DEFAULT_FRAME_BASE_URL = "https://elements.ozura.com";
978
+ const DEFAULT_FRAME_BASE_URL = "https://lively-hill-097170c0f.4.azurestaticapps.net";
881
979
  /**
882
980
  * The main entry point for OzElements. Creates and manages iframe-based
883
981
  * card input elements that keep raw card data isolated from the merchant page.
@@ -910,7 +1008,7 @@ class OzVault {
910
1008
  * @internal
911
1009
  */
912
1010
  constructor(options, waxKey, tokenizationSessionId) {
913
- var _a, _b, _c;
1011
+ var _a, _b, _c, _d, _e, _f;
914
1012
  this.elements = new Map();
915
1013
  this.elementsByType = new Map();
916
1014
  this.bankElementsByType = new Map();
@@ -923,6 +1021,9 @@ class OzVault {
923
1021
  this.tokenizerReady = false;
924
1022
  this._tokenizing = null;
925
1023
  this._destroyed = false;
1024
+ // Incremented every time reset() cancels an active tokenization so that
1025
+ // any in-flight wax-key refresh retry can detect it was superseded.
1026
+ this._resetCount = 0;
926
1027
  // Tracks successful tokenizations against the per-key call budget so the SDK
927
1028
  // can proactively refresh the wax key after it has been consumed rather than
928
1029
  // waiting for the next createToken() call to fail.
@@ -941,14 +1042,16 @@ class OzVault {
941
1042
  this.fonts = (_a = options.fonts) !== null && _a !== void 0 ? _a : [];
942
1043
  this.resolvedAppearance = resolveAppearance(options.appearance);
943
1044
  this.vaultId = `vault-${uuid()}`;
944
- this._maxTokenizeCalls = (_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3;
1045
+ // sessionLimit takes precedence over legacy maxTokenizeCalls
1046
+ this._maxTokenizeCalls = (_c = (_b = options.sessionLimit) !== null && _b !== void 0 ? _b : options.maxTokenizeCalls) !== null && _c !== void 0 ? _c : 3;
1047
+ this._debug = (_d = options.debug) !== null && _d !== void 0 ? _d : false;
945
1048
  this.boundHandleMessage = this.handleMessage.bind(this);
946
1049
  window.addEventListener('message', this.boundHandleMessage);
947
1050
  this.boundHandleVisibility = this.handleVisibilityChange.bind(this);
948
1051
  document.addEventListener('visibilitychange', this.boundHandleVisibility);
949
1052
  this.mountTokenizerFrame();
950
1053
  if (options.onLoadError) {
951
- const timeout = (_c = options.loadTimeoutMs) !== null && _c !== void 0 ? _c : 10000;
1054
+ const timeout = (_e = options.loadTimeoutMs) !== null && _e !== void 0 ? _e : 10000;
952
1055
  this.loadErrorTimeoutId = setTimeout(() => {
953
1056
  this.loadErrorTimeoutId = null;
954
1057
  if (!this._destroyed && !this.tokenizerReady) {
@@ -956,54 +1059,68 @@ class OzVault {
956
1059
  }
957
1060
  }, timeout);
958
1061
  }
959
- this._onWaxRefresh = options.onWaxRefresh;
1062
+ // onSessionRefresh takes precedence over legacy onWaxRefresh
1063
+ this._onWaxRefresh = (_f = options.onSessionRefresh) !== null && _f !== void 0 ? _f : options.onWaxRefresh;
960
1064
  this._onReady = options.onReady;
1065
+ this.log('vault created', { vaultId: this.vaultId, frameBaseUrl: this.frameBaseUrl, maxTokenizeCalls: this._maxTokenizeCalls });
961
1066
  }
962
1067
  /**
963
1068
  * Creates and returns a ready `OzVault` instance.
964
1069
  *
965
1070
  * Internally this:
966
- * 1. Generates a `tokenizationSessionId` (UUID).
1071
+ * 1. Generates a session UUID.
967
1072
  * 2. Starts loading the hidden tokenizer iframe immediately.
968
- * 3. Calls `options.fetchWaxKey(tokenizationSessionId)` concurrently — your
969
- * backend mints a session-bound wax key from the vault and returns it.
970
- * 4. Resolves with the vault instance once the wax key is stored. The iframe
1073
+ * 3. Fetches a session key from your backend concurrently — either via
1074
+ * `sessionUrl` (simplest), `getSessionKey` (custom headers/auth), or the
1075
+ * deprecated `fetchWaxKey` callback.
1076
+ * 4. Resolves with the vault instance once the session key is stored. The iframe
971
1077
  * has been loading the whole time, so `isReady` may already be true or
972
1078
  * will fire shortly after.
973
1079
  *
974
1080
  * The returned vault is ready to create elements immediately. `createToken()`
975
1081
  * additionally requires `vault.isReady` (tokenizer iframe loaded).
976
1082
  *
977
- * @throws {OzError} if `fetchWaxKey` throws, returns a non-string value, or returns an empty/whitespace-only string.
1083
+ * @throws {OzError} if the session fetch fails, times out, or returns an empty string.
978
1084
  */
979
1085
  static async create(options, signal) {
980
1086
  if (!options.pubKey || !options.pubKey.trim()) {
981
1087
  throw new OzError('pubKey is required in options. Obtain your public key from the Ozura admin.');
982
1088
  }
983
- if (typeof options.fetchWaxKey !== 'function') {
984
- throw new OzError('fetchWaxKey must be a function. See OzVault.create() docs for the expected signature.');
1089
+ // Normalize the session callback. Priority: sessionUrl > getSessionKey > fetchWaxKey (deprecated).
1090
+ // This allows merchants to use the clean new API without touching legacy code.
1091
+ let resolvedFetchKey;
1092
+ if (options.sessionUrl) {
1093
+ resolvedFetchKey = createSessionFetcher(options.sessionUrl);
1094
+ }
1095
+ else if (typeof options.getSessionKey === 'function') {
1096
+ resolvedFetchKey = options.getSessionKey;
1097
+ }
1098
+ else if (typeof options.fetchWaxKey === 'function') {
1099
+ resolvedFetchKey = options.fetchWaxKey;
1100
+ }
1101
+ else {
1102
+ throw new OzError('A session URL or callback is required. Pass sessionUrl, getSessionKey, or fetchWaxKey to OzVault.create().');
985
1103
  }
986
1104
  const tokenizationSessionId = uuid();
987
1105
  // Construct the vault immediately — this mounts the tokenizer iframe so it
988
- // starts loading while fetchWaxKey is in flight. The waxKey field starts
989
- // empty and is set below before create() returns.
1106
+ // starts loading while the session fetch is in flight.
990
1107
  const vault = new OzVault(options, '', tokenizationSessionId);
991
1108
  // If the caller provides an AbortSignal (e.g. React useEffect cleanup),
992
1109
  // destroy the vault immediately on abort so the tokenizer iframe and message
993
- // listener are removed synchronously rather than waiting for fetchWaxKey to
994
- // settle. This eliminates the brief double-iframe window in React StrictMode.
1110
+ // listener are removed synchronously rather than waiting for the session fetch
1111
+ // to settle. This eliminates the brief double-iframe window in React StrictMode.
995
1112
  const onAbort = () => vault.destroy();
996
1113
  signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onAbort, { once: true });
997
1114
  let waxKey;
998
1115
  try {
999
- waxKey = await options.fetchWaxKey(tokenizationSessionId);
1116
+ waxKey = await resolvedFetchKey(tokenizationSessionId);
1000
1117
  }
1001
1118
  catch (err) {
1002
1119
  signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
1003
1120
  vault.destroy();
1004
1121
  if (signal === null || signal === void 0 ? void 0 : signal.aborted)
1005
1122
  throw new OzError('OzVault.create() was cancelled.');
1006
- // Preserve errorCode/retryable from OzError (e.g. timeout/network from createFetchWaxKey)
1123
+ // Preserve errorCode/retryable from OzError (e.g. timeout/network from createSessionFetcher)
1007
1124
  // so callers can distinguish transient failures from config errors.
1008
1125
  const originalCode = err instanceof OzError ? err.errorCode : undefined;
1009
1126
  const msg = err instanceof Error ? err.message : 'Unknown error';
@@ -1020,13 +1137,14 @@ class OzVault {
1020
1137
  }
1021
1138
  // Static methods can access private fields of instances of the same class.
1022
1139
  vault.waxKey = waxKey;
1023
- vault._storedFetchWaxKey = options.fetchWaxKey;
1140
+ vault._storedFetchWaxKey = resolvedFetchKey;
1024
1141
  // If the tokenizer iframe fired OZ_FRAME_READY before fetchWaxKey resolved,
1025
1142
  // the OZ_INIT sent at that point had an empty waxKey. Send a follow-up now
1026
1143
  // so the tokenizer has the key stored before any createToken() call.
1027
1144
  if (vault.tokenizerReady) {
1028
1145
  vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey });
1029
1146
  }
1147
+ vault.log('wax key received — vault ready');
1030
1148
  return vault;
1031
1149
  }
1032
1150
  /**
@@ -1168,8 +1286,13 @@ class OzVault {
1168
1286
  const readyBankElements = [accountEl, routingEl];
1169
1287
  this._tokenizing = 'bank';
1170
1288
  const requestId = `req-${uuid()}`;
1289
+ this.log('createBankToken() called');
1171
1290
  return new Promise((resolve, reject) => {
1172
- const cleanup = () => { this._tokenizing = null; };
1291
+ const resetCountAtStart = this._resetCount;
1292
+ const cleanup = () => {
1293
+ if (this._resetCount === resetCountAtStart)
1294
+ this._tokenizing = null;
1295
+ };
1173
1296
  this.bankTokenizeResolvers.set(requestId, {
1174
1297
  resolve: (v) => { cleanup(); resolve(v); },
1175
1298
  reject: (e) => { cleanup(); reject(e); },
@@ -1180,6 +1303,7 @@ class OzVault {
1180
1303
  });
1181
1304
  try {
1182
1305
  const bankChannels = readyBankElements.map(() => new MessageChannel());
1306
+ const bankTokenizeStartMs = Date.now();
1183
1307
  this.sendToTokenizer({
1184
1308
  type: 'OZ_BANK_TOKENIZE',
1185
1309
  requestId,
@@ -1190,6 +1314,7 @@ class OzVault {
1190
1314
  lastName: options.lastName.trim(),
1191
1315
  fieldCount: readyBankElements.length,
1192
1316
  }, bankChannels.map(ch => ch.port1));
1317
+ this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
1193
1318
  readyBankElements.forEach((el, i) => el.beginCollect(requestId, bankChannels[i].port2));
1194
1319
  const bankTimeoutId = setTimeout(() => {
1195
1320
  if (this.bankTokenizeResolvers.has(requestId)) {
@@ -1200,8 +1325,10 @@ class OzVault {
1200
1325
  }
1201
1326
  }, 30000);
1202
1327
  const bankPendingEntry = this.bankTokenizeResolvers.get(requestId);
1203
- if (bankPendingEntry)
1328
+ if (bankPendingEntry) {
1204
1329
  bankPendingEntry.timeoutId = bankTimeoutId;
1330
+ bankPendingEntry.tokenizeStartMs = bankTokenizeStartMs;
1331
+ }
1205
1332
  }
1206
1333
  catch (err) {
1207
1334
  this.bankTokenizeResolvers.delete(requestId);
@@ -1272,8 +1399,15 @@ class OzVault {
1272
1399
  }
1273
1400
  this._tokenizing = 'card';
1274
1401
  const requestId = `req-${uuid()}`;
1402
+ this.log('createToken() called');
1275
1403
  return new Promise((resolve, reject) => {
1276
- const cleanup = () => { this._tokenizing = null; };
1404
+ // Capture the reset generation so cleanup() only zeros _tokenizing when it
1405
+ // still belongs to this invocation — not a newer one that started after a reset.
1406
+ const resetCountAtStart = this._resetCount;
1407
+ const cleanup = () => {
1408
+ if (this._resetCount === resetCountAtStart)
1409
+ this._tokenizing = null;
1410
+ };
1277
1411
  this.tokenizeResolvers.set(requestId, {
1278
1412
  resolve: (v) => { cleanup(); resolve(v); },
1279
1413
  reject: (e) => { cleanup(); reject(e); },
@@ -1286,6 +1420,7 @@ class OzVault {
1286
1420
  try {
1287
1421
  // Tell tokenizer frame to expect N field values, then tokenize
1288
1422
  const cardChannels = readyElements.map(() => new MessageChannel());
1423
+ const tokenizeStartMs = Date.now();
1289
1424
  this.sendToTokenizer({
1290
1425
  type: 'OZ_TOKENIZE',
1291
1426
  requestId,
@@ -1296,6 +1431,11 @@ class OzVault {
1296
1431
  lastName,
1297
1432
  fieldCount: readyElements.length,
1298
1433
  }, cardChannels.map(ch => ch.port1));
1434
+ this.log('OZ_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyElements.length });
1435
+ // Store start time for elapsed-ms logging on result
1436
+ const cardEntry = this.tokenizeResolvers.get(requestId);
1437
+ if (cardEntry)
1438
+ cardEntry.tokenizeStartMs = tokenizeStartMs;
1299
1439
  // Tell each ready element frame to send its raw value to the tokenizer
1300
1440
  readyElements.forEach((el, i) => el.beginCollect(requestId, cardChannels[i].port2));
1301
1441
  const cardTimeoutId = setTimeout(() => {
@@ -1317,6 +1457,63 @@ class OzVault {
1317
1457
  }
1318
1458
  });
1319
1459
  }
1460
+ /**
1461
+ * Clears all mounted element fields without tearing down the vault.
1462
+ *
1463
+ * Call this after a failed payment (e.g. card declined) to let the customer
1464
+ * re-enter their details. The vault instance, tokenizer iframe, wax key, and
1465
+ * tokenization budget counter are all preserved — no network calls are made.
1466
+ *
1467
+ * **Wax key session model:** by design, one wax key covers the full checkout
1468
+ * session. The default `max_tokenize_calls: 3` supports two declined attempts
1469
+ * and one final attempt on the same key. Do not call `vault.destroy()` and
1470
+ * recreate the vault between declines — that unnecessarily re-mints the key
1471
+ * and discards the remaining budget.
1472
+ *
1473
+ * @example
1474
+ * try {
1475
+ * const { token, cvcSession } = await vault.createToken({ billing });
1476
+ * await chargeCard(token, cvcSession);
1477
+ * } catch (err) {
1478
+ * vault.reset(); // clear fields; let customer re-enter
1479
+ * showError(err.message);
1480
+ * }
1481
+ */
1482
+ reset() {
1483
+ if (this._destroyed)
1484
+ return;
1485
+ const cancelling = Boolean(this._tokenizing);
1486
+ this.log('reset() called', { tokenizing: this._tokenizing, cancelling });
1487
+ if (this._tokenizing) {
1488
+ this._tokenizing = null;
1489
+ this._resetCount++;
1490
+ this.tokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1491
+ if (timeoutId != null)
1492
+ clearTimeout(timeoutId);
1493
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1494
+ reject(new OzError('Vault was reset while tokenization was in progress.'));
1495
+ });
1496
+ this.tokenizeResolvers.clear();
1497
+ this.bankTokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1498
+ if (timeoutId != null)
1499
+ clearTimeout(timeoutId);
1500
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1501
+ reject(new OzError('Vault was reset while tokenization was in progress.'));
1502
+ });
1503
+ this.bankTokenizeResolvers.clear();
1504
+ }
1505
+ // Clear field values in all mounted element iframes
1506
+ this.elementsByType.forEach(el => el.clear());
1507
+ this.bankElementsByType.forEach(el => el.clear());
1508
+ // Reset per-element completion state so auto-advance starts fresh on re-entry
1509
+ for (const frameId of this.completionState.keys()) {
1510
+ this.completionState.set(frameId, false);
1511
+ }
1512
+ // NOTE: _tokenizeSuccessCount is intentionally NOT reset.
1513
+ // It reflects real server-side wax key budget consumption. Zeroing it
1514
+ // would desync the proactive refresh logic from the vault's state and
1515
+ // risk triggering a mid-session re-mint on what should be a clean retry.
1516
+ }
1320
1517
  /**
1321
1518
  * Tears down the vault: removes all element iframes, the tokenizer iframe,
1322
1519
  * and the global message listener. Call this when the checkout component
@@ -1327,6 +1524,7 @@ class OzVault {
1327
1524
  if (this._destroyed)
1328
1525
  return;
1329
1526
  this._destroyed = true;
1527
+ this.log('destroy() called');
1330
1528
  window.removeEventListener('message', this.boundHandleMessage);
1331
1529
  document.removeEventListener('visibilitychange', this.boundHandleVisibility);
1332
1530
  if (this._pendingMount) {
@@ -1381,13 +1579,17 @@ class OzVault {
1381
1579
  const REFRESH_THRESHOLD_MS = 20 * 60 * 1000; // 20 minutes
1382
1580
  if (document.hidden) {
1383
1581
  this._hiddenAt = Date.now();
1582
+ this.log('tab hidden');
1384
1583
  }
1385
1584
  else {
1386
- if (this._hiddenAt !== null &&
1387
- Date.now() - this._hiddenAt >= REFRESH_THRESHOLD_MS &&
1388
- this._storedFetchWaxKey &&
1585
+ const hiddenMs = this._hiddenAt !== null ? Date.now() - this._hiddenAt : 0;
1586
+ const willRefresh = (this._hiddenAt !== null &&
1587
+ hiddenMs >= REFRESH_THRESHOLD_MS &&
1588
+ Boolean(this._storedFetchWaxKey) &&
1389
1589
  !this._tokenizing &&
1390
- !this._waxRefreshing) {
1590
+ !this._waxRefreshing);
1591
+ this.log('tab visible', { hiddenMs, willRefresh });
1592
+ if (willRefresh) {
1391
1593
  this.refreshWaxKey().catch((err) => {
1392
1594
  // Proactive refresh failure is non-fatal — the reactive path on the
1393
1595
  // next createToken() call will handle it, including the auth retry.
@@ -1397,6 +1599,56 @@ class OzVault {
1397
1599
  this._hiddenAt = null;
1398
1600
  }
1399
1601
  }
1602
+ // ─── Debug ───────────────────────────────────────────────────────────────
1603
+ /**
1604
+ * Emits a `[OzVault]`-prefixed entry to `console.log`. No-op when `debug` is
1605
+ * not set. Never called with sensitive values — callers use presence flags only.
1606
+ */
1607
+ log(message, data) {
1608
+ if (!this._debug)
1609
+ return;
1610
+ if (data !== undefined) {
1611
+ console.log(`[OzVault] ${message}`, data);
1612
+ }
1613
+ else {
1614
+ console.log(`[OzVault] ${message}`);
1615
+ }
1616
+ }
1617
+ /**
1618
+ * Returns a plain-object snapshot of the vault's current internal state.
1619
+ * Safe to attach to bug reports — no wax keys, tokens, or billing data included.
1620
+ *
1621
+ * Available on all vault instances regardless of whether `debug` was enabled.
1622
+ *
1623
+ * @example
1624
+ * console.log(vault.debugState());
1625
+ * // {
1626
+ * // vaultId: 'vault-abc123',
1627
+ * // isReady: true,
1628
+ * // tokenizing: null,
1629
+ * // destroyed: false,
1630
+ * // waxKeyPresent: true,
1631
+ * // elements: ['cardNumber', 'expirationDate', 'cvv'],
1632
+ * // ...
1633
+ * // }
1634
+ */
1635
+ debugState() {
1636
+ return {
1637
+ vaultId: this.vaultId,
1638
+ isReady: this.tokenizerReady,
1639
+ tokenizing: this._tokenizing,
1640
+ destroyed: this._destroyed,
1641
+ waxKeyPresent: Boolean(this.waxKey),
1642
+ tokenizeSuccessCount: this._tokenizeSuccessCount,
1643
+ maxTokenizeCalls: this._maxTokenizeCalls,
1644
+ resetCount: this._resetCount,
1645
+ elements: [...this.elementsByType.keys()],
1646
+ bankElements: [...this.bankElementsByType.keys()],
1647
+ completionState: Object.fromEntries([...this.completionState.entries()].map(([id, v]) => [id.slice(0, 8), v])),
1648
+ pendingTokenizations: this.tokenizeResolvers.size,
1649
+ pendingBankTokenizations: this.bankTokenizeResolvers.size,
1650
+ };
1651
+ }
1400
1652
  mountTokenizerFrame() {
1401
1653
  const mount = () => {
1402
1654
  this._pendingMount = null;
@@ -1408,6 +1660,7 @@ class OzVault {
1408
1660
  iframe.src = `${this.frameBaseUrl}/frame/tokenizer-frame.html#vaultId=${encodeURIComponent(this.vaultId)}${parentOrigin ? `&parentOrigin=${encodeURIComponent(parentOrigin)}` : ''}`;
1409
1661
  document.body.appendChild(iframe);
1410
1662
  this.tokenizerFrame = iframe;
1663
+ this.log('mounting tokenizer iframe');
1411
1664
  };
1412
1665
  if (document.readyState === 'loading') {
1413
1666
  this._pendingMount = mount;
@@ -1443,6 +1696,12 @@ class OzVault {
1443
1696
  // the previous session and justCompleted never fires, breaking auto-advance.
1444
1697
  if (msg.type === 'OZ_FRAME_READY') {
1445
1698
  this.completionState.set(frameId, false);
1699
+ if (msg.__ozVersion !== PROTOCOL_VERSION) {
1700
+ console.warn(`[OzVault] Protocol version mismatch on element frame "${frameId}" — ` +
1701
+ `SDK expects v${PROTOCOL_VERSION}, frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
1702
+ 'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
1703
+ }
1704
+ this.log('element iframe ready', { type: el.type, frameIdPrefix: frameId.slice(0, 8) });
1446
1705
  }
1447
1706
  // Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
1448
1707
  if (msg.type === 'OZ_CHANGE') {
@@ -1466,6 +1725,7 @@ class OzVault {
1466
1725
  // Require valid too — avoids advancing at 13 digits for unknown-brand cards
1467
1726
  // where isComplete() fires before the user has finished typing.
1468
1727
  const justCompleted = complete && valid && !wasComplete;
1728
+ this.log('field changed', { type: el.type, complete, valid, justCompleted });
1469
1729
  // Sync CVV length when card brand changes
1470
1730
  if (el.type === 'cardNumber') {
1471
1731
  const brand = msg.cardBrand;
@@ -1477,17 +1737,25 @@ class OzVault {
1477
1737
  // Auto-advance focus on completion
1478
1738
  if (justCompleted) {
1479
1739
  if (el.type === 'cardNumber') {
1740
+ this.log('auto-advance', { from: 'cardNumber', to: 'expirationDate' });
1480
1741
  (_b = this.elementsByType.get('expirationDate')) === null || _b === void 0 ? void 0 : _b.focus();
1481
1742
  }
1482
1743
  else if (el.type === 'expirationDate') {
1744
+ this.log('auto-advance', { from: 'expirationDate', to: 'cvv' });
1483
1745
  (_c = this.elementsByType.get('cvv')) === null || _c === void 0 ? void 0 : _c.focus();
1484
1746
  }
1485
1747
  }
1486
1748
  }
1487
1749
  handleTokenizerMessage(msg) {
1488
- var _a, _b, _c;
1750
+ var _a, _b, _c, _d;
1489
1751
  switch (msg.type) {
1490
1752
  case 'OZ_FRAME_READY':
1753
+ if (msg.__ozVersion !== PROTOCOL_VERSION) {
1754
+ console.warn(`[OzVault] Protocol version mismatch — SDK expects v${PROTOCOL_VERSION}, ` +
1755
+ `tokenizer frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
1756
+ 'This usually means the deployed frame files are stale. ' +
1757
+ 'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
1758
+ }
1491
1759
  this.tokenizerReady = true;
1492
1760
  if (this.loadErrorTimeoutId != null) {
1493
1761
  clearTimeout(this.loadErrorTimeoutId);
@@ -1499,6 +1767,7 @@ class OzVault {
1499
1767
  // sent again from create() once the key is available.
1500
1768
  this.sendToTokenizer(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})));
1501
1769
  (_c = this._onReady) === null || _c === void 0 ? void 0 : _c.call(this);
1770
+ this.log('tokenizer iframe ready', { protocolVersion: (_d = msg.__ozVersion) !== null && _d !== void 0 ? _d : null });
1502
1771
  break;
1503
1772
  case 'OZ_TOKEN_RESULT': {
1504
1773
  if (typeof msg.requestId !== 'string' || !msg.requestId) {
@@ -1523,11 +1792,18 @@ class OzVault {
1523
1792
  }
1524
1793
  pending.resolve(Object.assign(Object.assign({ token,
1525
1794
  cvcSession }, (card ? { card } : {})), (pending.billing ? { billing: pending.billing } : {})));
1795
+ this.log('token received', {
1796
+ elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
1797
+ tokenPresent: true,
1798
+ cvcSessionPresent: true,
1799
+ cardMetadataPresent: Boolean(card),
1800
+ });
1526
1801
  // Increment the per-key success counter and proactively refresh once
1527
1802
  // the budget is exhausted so the next createToken() call uses a fresh
1528
1803
  // key without waiting for a vault rejection.
1529
1804
  this._tokenizeSuccessCount++;
1530
1805
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1806
+ this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1531
1807
  this.refreshWaxKey().catch((err) => {
1532
1808
  console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1533
1809
  });
@@ -1547,14 +1823,25 @@ class OzVault {
1547
1823
  this.tokenizeResolvers.delete(msg.requestId);
1548
1824
  if (pending.timeoutId != null)
1549
1825
  clearTimeout(pending.timeoutId);
1826
+ const willRefresh = this.isRefreshableAuthError(errorCode, raw) && !pending.retried && Boolean(this._storedFetchWaxKey);
1827
+ this.log('token error', { errorCode, willRefresh });
1550
1828
  // Auto-refresh: if the wax key expired or was consumed and we haven't
1551
1829
  // already retried for this request, transparently re-mint and retry.
1552
- if (this.isRefreshableAuthError(errorCode, raw) && !pending.retried && this._storedFetchWaxKey) {
1830
+ if (willRefresh) {
1831
+ const resetCountAtRetry = this._resetCount;
1553
1832
  this.refreshWaxKey().then(() => {
1554
1833
  if (this._destroyed) {
1555
1834
  pending.reject(new OzError('Vault destroyed during wax key refresh.'));
1556
1835
  return;
1557
1836
  }
1837
+ if (this._resetCount !== resetCountAtRetry) {
1838
+ // reset() was called while the wax key was refreshing — the fields
1839
+ // have been cleared and _tokenizing was zeroed. Reject the original
1840
+ // promise so it doesn't stay pending, and bail out without starting
1841
+ // a new retry (which would tokenize against empty fields).
1842
+ pending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1843
+ return;
1844
+ }
1558
1845
  const newRequestId = `req-${uuid()}`;
1559
1846
  // _tokenizing is still 'card' (cleanup() hasn't been called yet)
1560
1847
  this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
@@ -1601,11 +1888,16 @@ class OzVault {
1601
1888
  if (bankPending.timeoutId != null)
1602
1889
  clearTimeout(bankPending.timeoutId);
1603
1890
  if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
1891
+ const resetCountAtRetry = this._resetCount;
1604
1892
  this.refreshWaxKey().then(() => {
1605
1893
  if (this._destroyed) {
1606
1894
  bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
1607
1895
  return;
1608
1896
  }
1897
+ if (this._resetCount !== resetCountAtRetry) {
1898
+ bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1899
+ return;
1900
+ }
1609
1901
  const newRequestId = `req-${uuid()}`;
1610
1902
  this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
1611
1903
  try {
@@ -1663,9 +1955,15 @@ class OzVault {
1663
1955
  }
1664
1956
  const bank = isBankAccountMetadata(msg.bank) ? msg.bank : undefined;
1665
1957
  pending.resolve(Object.assign({ token }, (bank ? { bank } : {})));
1958
+ this.log('bank token received', {
1959
+ elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
1960
+ tokenPresent: true,
1961
+ bankMetadataPresent: Boolean(bank),
1962
+ });
1666
1963
  // Same proactive refresh logic as card tokenization.
1667
1964
  this._tokenizeSuccessCount++;
1668
1965
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1966
+ this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1669
1967
  this.refreshWaxKey().catch((err) => {
1670
1968
  console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1671
1969
  });
@@ -1721,6 +2019,7 @@ class OzVault {
1721
2019
  }
1722
2020
  const newSessionId = uuid();
1723
2021
  (_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
2022
+ this.log('wax key refresh started');
1724
2023
  this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
1725
2024
  .then(newWaxKey => {
1726
2025
  if (typeof newWaxKey !== 'string' || !newWaxKey.trim()) {
@@ -1734,6 +2033,11 @@ class OzVault {
1734
2033
  if (!this._destroyed && this.tokenizerReady) {
1735
2034
  this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey });
1736
2035
  }
2036
+ this.log('wax key refresh succeeded');
2037
+ })
2038
+ .catch((err) => {
2039
+ this.log('wax key refresh failed', { error: err instanceof Error ? err.message : String(err) });
2040
+ throw err;
1737
2041
  })
1738
2042
  .finally(() => {
1739
2043
  this._waxRefreshing = null;
@@ -1747,83 +2051,5 @@ class OzVault {
1747
2051
  }
1748
2052
  }
1749
2053
 
1750
- /**
1751
- * Creates a ready-to-use `fetchWaxKey` callback for `OzVault.create()` and `<OzElements>`.
1752
- *
1753
- * Calls your backend mint endpoint with `{ sessionId }` and returns the wax key string.
1754
- * Throws on non-OK responses or a missing `waxKey` field so the vault can surface the
1755
- * error through its normal error path.
1756
- *
1757
- * Each call enforces a 10-second per-attempt timeout. On a pure network-level
1758
- * failure (connection refused, DNS failure, etc.) the call is retried once after
1759
- * 750ms before throwing. HTTP errors (4xx/5xx) are never retried — they indicate
1760
- * an endpoint misconfiguration or an invalid key, not a transient failure.
1761
- *
1762
- * The mint endpoint is typically the one-line `createMintWaxHandler` / `createMintWaxMiddleware`
1763
- * from `@ozura/elements/server`.
1764
- *
1765
- * @param mintUrl - Absolute or relative URL of your wax-key mint endpoint, e.g. `'/api/mint-wax'`.
1766
- *
1767
- * @example
1768
- * // Vanilla JS
1769
- * const vault = await OzVault.create({
1770
- * pubKey: 'pk_live_...',
1771
- * fetchWaxKey: createFetchWaxKey('/api/mint-wax'),
1772
- * });
1773
- *
1774
- * @example
1775
- * // React
1776
- * <OzElements pubKey="pk_live_..." fetchWaxKey={createFetchWaxKey('/api/mint-wax')}>
1777
- */
1778
- function createFetchWaxKey(mintUrl) {
1779
- const TIMEOUT_MS = 10000;
1780
- // Each attempt gets its own AbortController so a timeout on attempt 1 does
1781
- // not bleed into the retry. Uses AbortController + setTimeout instead of
1782
- // AbortSignal.timeout() to support environments without that API.
1783
- const attemptFetch = (sessionId) => {
1784
- const controller = new AbortController();
1785
- const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
1786
- return fetch(mintUrl, {
1787
- method: 'POST',
1788
- headers: { 'Content-Type': 'application/json' },
1789
- body: JSON.stringify({ sessionId }),
1790
- signal: controller.signal,
1791
- }).finally(() => clearTimeout(timer));
1792
- };
1793
- return async (sessionId) => {
1794
- let res;
1795
- try {
1796
- res = await attemptFetch(sessionId);
1797
- }
1798
- catch (firstErr) {
1799
- // Abort/timeout should not be retried — the server received nothing or
1800
- // we already waited the full timeout duration.
1801
- if (firstErr instanceof Error && (firstErr.name === 'AbortError' || firstErr.name === 'TimeoutError')) {
1802
- throw new OzError(`Wax key mint timed out after ${TIMEOUT_MS / 1000}s (${mintUrl})`, undefined, 'timeout');
1803
- }
1804
- // Pure network error (offline, DNS, connection refused) — retry once
1805
- // after a short pause in case of a transient blip.
1806
- await new Promise(resolve => setTimeout(resolve, 750));
1807
- try {
1808
- res = await attemptFetch(sessionId);
1809
- }
1810
- catch (retryErr) {
1811
- const msg = retryErr instanceof Error ? retryErr.message : 'Network error';
1812
- throw new OzError(`Could not reach wax key mint endpoint (${mintUrl}): ${msg}`, undefined, 'network');
1813
- }
1814
- }
1815
- const data = await res.json().catch(() => ({}));
1816
- if (!res.ok) {
1817
- throw new OzError(typeof data.error === 'string' && data.error
1818
- ? data.error
1819
- : `Wax key mint failed (HTTP ${res.status})`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
1820
- }
1821
- if (typeof data.waxKey !== 'string' || !data.waxKey.trim()) {
1822
- throw new OzError('Mint endpoint response is missing waxKey. Check your /api/mint-wax implementation.', undefined, 'validation');
1823
- }
1824
- return data.waxKey;
1825
- };
1826
- }
1827
-
1828
- export { OzElement, OzError, OzVault, createFetchWaxKey, normalizeBankVaultError, normalizeCardSaleError, normalizeVaultError };
2054
+ export { OzElement, OzError, OzVault, createSessionFetcher as createFetchWaxKey, createSessionFetcher, normalizeBankVaultError, normalizeCardSaleError, normalizeVaultError };
1829
2055
  //# sourceMappingURL=oz-elements.esm.js.map