@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
@@ -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
  *
@@ -413,7 +413,7 @@
413
413
  * (useful when integrating with React refs).
414
414
  */
415
415
  mount(target) {
416
- var _a;
416
+ var _a, _b;
417
417
  if (this._destroyed)
418
418
  throw new OzError('Cannot mount a destroyed element.');
419
419
  if (this.iframe)
@@ -430,7 +430,13 @@
430
430
  iframe.setAttribute('scrolling', 'no');
431
431
  iframe.setAttribute('allowtransparency', 'true');
432
432
  iframe.style.cssText = 'border:none;width:100%;height:46px;display:block;overflow:hidden;';
433
- iframe.title = `Secure ${this.elementType} input`;
433
+ iframe.title = `Secure ${(_a = {
434
+ cardNumber: 'card number',
435
+ expirationDate: 'expiration date',
436
+ cvv: 'CVV',
437
+ accountNumber: 'account number',
438
+ routingNumber: 'routing number',
439
+ }[this.elementType]) !== null && _a !== void 0 ? _a : this.elementType} input`;
434
440
  // Note: the `sandbox` attribute is intentionally NOT set. Field values are
435
441
  // delivered to the tokenizer iframe via a MessageChannel port (transferred
436
442
  // in OZ_BEGIN_COLLECT), so no window.parent named-frame lookup is needed.
@@ -444,7 +450,7 @@
444
450
  container.appendChild(iframe);
445
451
  this.iframe = iframe;
446
452
  this._frameWindow = iframe.contentWindow;
447
- const timeout = (_a = this.options.loadTimeoutMs) !== null && _a !== void 0 ? _a : 10000;
453
+ const timeout = (_b = this.options.loadTimeoutMs) !== null && _b !== void 0 ? _b : 10000;
448
454
  this._loadTimer = setTimeout(() => {
449
455
  if (!this._ready && !this._destroyed) {
450
456
  this.emit('loaderror', { elementType: this.elementType, error: `${this.elementType} iframe failed to load within ${timeout}ms` });
@@ -877,13 +883,105 @@
877
883
  return { valid: errors.length === 0, errors, normalized };
878
884
  }
879
885
 
886
+ /**
887
+ * Shared postMessage protocol constants.
888
+ *
889
+ * PROTOCOL_VERSION must be incremented any time a breaking change is made to
890
+ * the postMessage message shape (new required fields, renamed types, removed
891
+ * fields, changed semantics). The SDK reads this value from OZ_FRAME_READY
892
+ * messages and warns when the frame and SDK are out of sync.
893
+ *
894
+ * Non-breaking additions (new optional fields, new message types that old
895
+ * frames can safely ignore) do NOT require a version bump.
896
+ */
897
+ const PROTOCOL_VERSION = 1;
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
+
880
978
  function isCardMetadata(v) {
881
979
  return !!v && typeof v === 'object' && typeof v.last4 === 'string';
882
980
  }
883
981
  function isBankAccountMetadata(v) {
884
982
  return !!v && typeof v === 'object' && typeof v.last4 === 'string';
885
983
  }
886
- const DEFAULT_FRAME_BASE_URL = "https://elements.ozura.com";
984
+ const DEFAULT_FRAME_BASE_URL = "https://lively-hill-097170c0f.4.azurestaticapps.net";
887
985
  /**
888
986
  * The main entry point for OzElements. Creates and manages iframe-based
889
987
  * card input elements that keep raw card data isolated from the merchant page.
@@ -916,7 +1014,7 @@
916
1014
  * @internal
917
1015
  */
918
1016
  constructor(options, waxKey, tokenizationSessionId) {
919
- var _a, _b, _c;
1017
+ var _a, _b, _c, _d, _e, _f;
920
1018
  this.elements = new Map();
921
1019
  this.elementsByType = new Map();
922
1020
  this.bankElementsByType = new Map();
@@ -929,6 +1027,9 @@
929
1027
  this.tokenizerReady = false;
930
1028
  this._tokenizing = null;
931
1029
  this._destroyed = false;
1030
+ // Incremented every time reset() cancels an active tokenization so that
1031
+ // any in-flight wax-key refresh retry can detect it was superseded.
1032
+ this._resetCount = 0;
932
1033
  // Tracks successful tokenizations against the per-key call budget so the SDK
933
1034
  // can proactively refresh the wax key after it has been consumed rather than
934
1035
  // waiting for the next createToken() call to fail.
@@ -947,14 +1048,16 @@
947
1048
  this.fonts = (_a = options.fonts) !== null && _a !== void 0 ? _a : [];
948
1049
  this.resolvedAppearance = resolveAppearance(options.appearance);
949
1050
  this.vaultId = `vault-${uuid()}`;
950
- this._maxTokenizeCalls = (_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3;
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;
951
1054
  this.boundHandleMessage = this.handleMessage.bind(this);
952
1055
  window.addEventListener('message', this.boundHandleMessage);
953
1056
  this.boundHandleVisibility = this.handleVisibilityChange.bind(this);
954
1057
  document.addEventListener('visibilitychange', this.boundHandleVisibility);
955
1058
  this.mountTokenizerFrame();
956
1059
  if (options.onLoadError) {
957
- const timeout = (_c = options.loadTimeoutMs) !== null && _c !== void 0 ? _c : 10000;
1060
+ const timeout = (_e = options.loadTimeoutMs) !== null && _e !== void 0 ? _e : 10000;
958
1061
  this.loadErrorTimeoutId = setTimeout(() => {
959
1062
  this.loadErrorTimeoutId = null;
960
1063
  if (!this._destroyed && !this.tokenizerReady) {
@@ -962,54 +1065,68 @@
962
1065
  }
963
1066
  }, timeout);
964
1067
  }
965
- this._onWaxRefresh = options.onWaxRefresh;
1068
+ // onSessionRefresh takes precedence over legacy onWaxRefresh
1069
+ this._onWaxRefresh = (_f = options.onSessionRefresh) !== null && _f !== void 0 ? _f : options.onWaxRefresh;
966
1070
  this._onReady = options.onReady;
1071
+ this.log('vault created', { vaultId: this.vaultId, frameBaseUrl: this.frameBaseUrl, maxTokenizeCalls: this._maxTokenizeCalls });
967
1072
  }
968
1073
  /**
969
1074
  * Creates and returns a ready `OzVault` instance.
970
1075
  *
971
1076
  * Internally this:
972
- * 1. Generates a `tokenizationSessionId` (UUID).
1077
+ * 1. Generates a session UUID.
973
1078
  * 2. Starts loading the hidden tokenizer iframe immediately.
974
- * 3. Calls `options.fetchWaxKey(tokenizationSessionId)` concurrently — your
975
- * backend mints a session-bound wax key from the vault and returns it.
976
- * 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
977
1083
  * has been loading the whole time, so `isReady` may already be true or
978
1084
  * will fire shortly after.
979
1085
  *
980
1086
  * The returned vault is ready to create elements immediately. `createToken()`
981
1087
  * additionally requires `vault.isReady` (tokenizer iframe loaded).
982
1088
  *
983
- * @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.
984
1090
  */
985
1091
  static async create(options, signal) {
986
1092
  if (!options.pubKey || !options.pubKey.trim()) {
987
1093
  throw new OzError('pubKey is required in options. Obtain your public key from the Ozura admin.');
988
1094
  }
989
- if (typeof options.fetchWaxKey !== 'function') {
990
- 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().');
991
1109
  }
992
1110
  const tokenizationSessionId = uuid();
993
1111
  // Construct the vault immediately — this mounts the tokenizer iframe so it
994
- // starts loading while fetchWaxKey is in flight. The waxKey field starts
995
- // empty and is set below before create() returns.
1112
+ // starts loading while the session fetch is in flight.
996
1113
  const vault = new OzVault(options, '', tokenizationSessionId);
997
1114
  // If the caller provides an AbortSignal (e.g. React useEffect cleanup),
998
1115
  // destroy the vault immediately on abort so the tokenizer iframe and message
999
- // listener are removed synchronously rather than waiting for fetchWaxKey to
1000
- // 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.
1001
1118
  const onAbort = () => vault.destroy();
1002
1119
  signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onAbort, { once: true });
1003
1120
  let waxKey;
1004
1121
  try {
1005
- waxKey = await options.fetchWaxKey(tokenizationSessionId);
1122
+ waxKey = await resolvedFetchKey(tokenizationSessionId);
1006
1123
  }
1007
1124
  catch (err) {
1008
1125
  signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
1009
1126
  vault.destroy();
1010
1127
  if (signal === null || signal === void 0 ? void 0 : signal.aborted)
1011
1128
  throw new OzError('OzVault.create() was cancelled.');
1012
- // Preserve errorCode/retryable from OzError (e.g. timeout/network from createFetchWaxKey)
1129
+ // Preserve errorCode/retryable from OzError (e.g. timeout/network from createSessionFetcher)
1013
1130
  // so callers can distinguish transient failures from config errors.
1014
1131
  const originalCode = err instanceof OzError ? err.errorCode : undefined;
1015
1132
  const msg = err instanceof Error ? err.message : 'Unknown error';
@@ -1026,13 +1143,14 @@
1026
1143
  }
1027
1144
  // Static methods can access private fields of instances of the same class.
1028
1145
  vault.waxKey = waxKey;
1029
- vault._storedFetchWaxKey = options.fetchWaxKey;
1146
+ vault._storedFetchWaxKey = resolvedFetchKey;
1030
1147
  // If the tokenizer iframe fired OZ_FRAME_READY before fetchWaxKey resolved,
1031
1148
  // the OZ_INIT sent at that point had an empty waxKey. Send a follow-up now
1032
1149
  // so the tokenizer has the key stored before any createToken() call.
1033
1150
  if (vault.tokenizerReady) {
1034
1151
  vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey });
1035
1152
  }
1153
+ vault.log('wax key received — vault ready');
1036
1154
  return vault;
1037
1155
  }
1038
1156
  /**
@@ -1174,8 +1292,13 @@
1174
1292
  const readyBankElements = [accountEl, routingEl];
1175
1293
  this._tokenizing = 'bank';
1176
1294
  const requestId = `req-${uuid()}`;
1295
+ this.log('createBankToken() called');
1177
1296
  return new Promise((resolve, reject) => {
1178
- const cleanup = () => { this._tokenizing = null; };
1297
+ const resetCountAtStart = this._resetCount;
1298
+ const cleanup = () => {
1299
+ if (this._resetCount === resetCountAtStart)
1300
+ this._tokenizing = null;
1301
+ };
1179
1302
  this.bankTokenizeResolvers.set(requestId, {
1180
1303
  resolve: (v) => { cleanup(); resolve(v); },
1181
1304
  reject: (e) => { cleanup(); reject(e); },
@@ -1186,6 +1309,7 @@
1186
1309
  });
1187
1310
  try {
1188
1311
  const bankChannels = readyBankElements.map(() => new MessageChannel());
1312
+ const bankTokenizeStartMs = Date.now();
1189
1313
  this.sendToTokenizer({
1190
1314
  type: 'OZ_BANK_TOKENIZE',
1191
1315
  requestId,
@@ -1196,6 +1320,7 @@
1196
1320
  lastName: options.lastName.trim(),
1197
1321
  fieldCount: readyBankElements.length,
1198
1322
  }, bankChannels.map(ch => ch.port1));
1323
+ this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
1199
1324
  readyBankElements.forEach((el, i) => el.beginCollect(requestId, bankChannels[i].port2));
1200
1325
  const bankTimeoutId = setTimeout(() => {
1201
1326
  if (this.bankTokenizeResolvers.has(requestId)) {
@@ -1206,8 +1331,10 @@
1206
1331
  }
1207
1332
  }, 30000);
1208
1333
  const bankPendingEntry = this.bankTokenizeResolvers.get(requestId);
1209
- if (bankPendingEntry)
1334
+ if (bankPendingEntry) {
1210
1335
  bankPendingEntry.timeoutId = bankTimeoutId;
1336
+ bankPendingEntry.tokenizeStartMs = bankTokenizeStartMs;
1337
+ }
1211
1338
  }
1212
1339
  catch (err) {
1213
1340
  this.bankTokenizeResolvers.delete(requestId);
@@ -1278,8 +1405,15 @@
1278
1405
  }
1279
1406
  this._tokenizing = 'card';
1280
1407
  const requestId = `req-${uuid()}`;
1408
+ this.log('createToken() called');
1281
1409
  return new Promise((resolve, reject) => {
1282
- const cleanup = () => { this._tokenizing = null; };
1410
+ // Capture the reset generation so cleanup() only zeros _tokenizing when it
1411
+ // still belongs to this invocation — not a newer one that started after a reset.
1412
+ const resetCountAtStart = this._resetCount;
1413
+ const cleanup = () => {
1414
+ if (this._resetCount === resetCountAtStart)
1415
+ this._tokenizing = null;
1416
+ };
1283
1417
  this.tokenizeResolvers.set(requestId, {
1284
1418
  resolve: (v) => { cleanup(); resolve(v); },
1285
1419
  reject: (e) => { cleanup(); reject(e); },
@@ -1292,6 +1426,7 @@
1292
1426
  try {
1293
1427
  // Tell tokenizer frame to expect N field values, then tokenize
1294
1428
  const cardChannels = readyElements.map(() => new MessageChannel());
1429
+ const tokenizeStartMs = Date.now();
1295
1430
  this.sendToTokenizer({
1296
1431
  type: 'OZ_TOKENIZE',
1297
1432
  requestId,
@@ -1302,6 +1437,11 @@
1302
1437
  lastName,
1303
1438
  fieldCount: readyElements.length,
1304
1439
  }, cardChannels.map(ch => ch.port1));
1440
+ this.log('OZ_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyElements.length });
1441
+ // Store start time for elapsed-ms logging on result
1442
+ const cardEntry = this.tokenizeResolvers.get(requestId);
1443
+ if (cardEntry)
1444
+ cardEntry.tokenizeStartMs = tokenizeStartMs;
1305
1445
  // Tell each ready element frame to send its raw value to the tokenizer
1306
1446
  readyElements.forEach((el, i) => el.beginCollect(requestId, cardChannels[i].port2));
1307
1447
  const cardTimeoutId = setTimeout(() => {
@@ -1323,6 +1463,63 @@
1323
1463
  }
1324
1464
  });
1325
1465
  }
1466
+ /**
1467
+ * Clears all mounted element fields without tearing down the vault.
1468
+ *
1469
+ * Call this after a failed payment (e.g. card declined) to let the customer
1470
+ * re-enter their details. The vault instance, tokenizer iframe, wax key, and
1471
+ * tokenization budget counter are all preserved — no network calls are made.
1472
+ *
1473
+ * **Wax key session model:** by design, one wax key covers the full checkout
1474
+ * session. The default `max_tokenize_calls: 3` supports two declined attempts
1475
+ * and one final attempt on the same key. Do not call `vault.destroy()` and
1476
+ * recreate the vault between declines — that unnecessarily re-mints the key
1477
+ * and discards the remaining budget.
1478
+ *
1479
+ * @example
1480
+ * try {
1481
+ * const { token, cvcSession } = await vault.createToken({ billing });
1482
+ * await chargeCard(token, cvcSession);
1483
+ * } catch (err) {
1484
+ * vault.reset(); // clear fields; let customer re-enter
1485
+ * showError(err.message);
1486
+ * }
1487
+ */
1488
+ reset() {
1489
+ if (this._destroyed)
1490
+ return;
1491
+ const cancelling = Boolean(this._tokenizing);
1492
+ this.log('reset() called', { tokenizing: this._tokenizing, cancelling });
1493
+ if (this._tokenizing) {
1494
+ this._tokenizing = null;
1495
+ this._resetCount++;
1496
+ this.tokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1497
+ if (timeoutId != null)
1498
+ clearTimeout(timeoutId);
1499
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1500
+ reject(new OzError('Vault was reset while tokenization was in progress.'));
1501
+ });
1502
+ this.tokenizeResolvers.clear();
1503
+ this.bankTokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1504
+ if (timeoutId != null)
1505
+ clearTimeout(timeoutId);
1506
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1507
+ reject(new OzError('Vault was reset while tokenization was in progress.'));
1508
+ });
1509
+ this.bankTokenizeResolvers.clear();
1510
+ }
1511
+ // Clear field values in all mounted element iframes
1512
+ this.elementsByType.forEach(el => el.clear());
1513
+ this.bankElementsByType.forEach(el => el.clear());
1514
+ // Reset per-element completion state so auto-advance starts fresh on re-entry
1515
+ for (const frameId of this.completionState.keys()) {
1516
+ this.completionState.set(frameId, false);
1517
+ }
1518
+ // NOTE: _tokenizeSuccessCount is intentionally NOT reset.
1519
+ // It reflects real server-side wax key budget consumption. Zeroing it
1520
+ // would desync the proactive refresh logic from the vault's state and
1521
+ // risk triggering a mid-session re-mint on what should be a clean retry.
1522
+ }
1326
1523
  /**
1327
1524
  * Tears down the vault: removes all element iframes, the tokenizer iframe,
1328
1525
  * and the global message listener. Call this when the checkout component
@@ -1333,6 +1530,7 @@
1333
1530
  if (this._destroyed)
1334
1531
  return;
1335
1532
  this._destroyed = true;
1533
+ this.log('destroy() called');
1336
1534
  window.removeEventListener('message', this.boundHandleMessage);
1337
1535
  document.removeEventListener('visibilitychange', this.boundHandleVisibility);
1338
1536
  if (this._pendingMount) {
@@ -1387,13 +1585,17 @@
1387
1585
  const REFRESH_THRESHOLD_MS = 20 * 60 * 1000; // 20 minutes
1388
1586
  if (document.hidden) {
1389
1587
  this._hiddenAt = Date.now();
1588
+ this.log('tab hidden');
1390
1589
  }
1391
1590
  else {
1392
- if (this._hiddenAt !== null &&
1393
- Date.now() - this._hiddenAt >= REFRESH_THRESHOLD_MS &&
1394
- this._storedFetchWaxKey &&
1591
+ const hiddenMs = this._hiddenAt !== null ? Date.now() - this._hiddenAt : 0;
1592
+ const willRefresh = (this._hiddenAt !== null &&
1593
+ hiddenMs >= REFRESH_THRESHOLD_MS &&
1594
+ Boolean(this._storedFetchWaxKey) &&
1395
1595
  !this._tokenizing &&
1396
- !this._waxRefreshing) {
1596
+ !this._waxRefreshing);
1597
+ this.log('tab visible', { hiddenMs, willRefresh });
1598
+ if (willRefresh) {
1397
1599
  this.refreshWaxKey().catch((err) => {
1398
1600
  // Proactive refresh failure is non-fatal — the reactive path on the
1399
1601
  // next createToken() call will handle it, including the auth retry.
@@ -1403,6 +1605,56 @@
1403
1605
  this._hiddenAt = null;
1404
1606
  }
1405
1607
  }
1608
+ // ─── Debug ───────────────────────────────────────────────────────────────
1609
+ /**
1610
+ * Emits a `[OzVault]`-prefixed entry to `console.log`. No-op when `debug` is
1611
+ * not set. Never called with sensitive values — callers use presence flags only.
1612
+ */
1613
+ log(message, data) {
1614
+ if (!this._debug)
1615
+ return;
1616
+ if (data !== undefined) {
1617
+ console.log(`[OzVault] ${message}`, data);
1618
+ }
1619
+ else {
1620
+ console.log(`[OzVault] ${message}`);
1621
+ }
1622
+ }
1623
+ /**
1624
+ * Returns a plain-object snapshot of the vault's current internal state.
1625
+ * Safe to attach to bug reports — no wax keys, tokens, or billing data included.
1626
+ *
1627
+ * Available on all vault instances regardless of whether `debug` was enabled.
1628
+ *
1629
+ * @example
1630
+ * console.log(vault.debugState());
1631
+ * // {
1632
+ * // vaultId: 'vault-abc123',
1633
+ * // isReady: true,
1634
+ * // tokenizing: null,
1635
+ * // destroyed: false,
1636
+ * // waxKeyPresent: true,
1637
+ * // elements: ['cardNumber', 'expirationDate', 'cvv'],
1638
+ * // ...
1639
+ * // }
1640
+ */
1641
+ debugState() {
1642
+ return {
1643
+ vaultId: this.vaultId,
1644
+ isReady: this.tokenizerReady,
1645
+ tokenizing: this._tokenizing,
1646
+ destroyed: this._destroyed,
1647
+ waxKeyPresent: Boolean(this.waxKey),
1648
+ tokenizeSuccessCount: this._tokenizeSuccessCount,
1649
+ maxTokenizeCalls: this._maxTokenizeCalls,
1650
+ resetCount: this._resetCount,
1651
+ elements: [...this.elementsByType.keys()],
1652
+ bankElements: [...this.bankElementsByType.keys()],
1653
+ completionState: Object.fromEntries([...this.completionState.entries()].map(([id, v]) => [id.slice(0, 8), v])),
1654
+ pendingTokenizations: this.tokenizeResolvers.size,
1655
+ pendingBankTokenizations: this.bankTokenizeResolvers.size,
1656
+ };
1657
+ }
1406
1658
  mountTokenizerFrame() {
1407
1659
  const mount = () => {
1408
1660
  this._pendingMount = null;
@@ -1414,6 +1666,7 @@
1414
1666
  iframe.src = `${this.frameBaseUrl}/frame/tokenizer-frame.html#vaultId=${encodeURIComponent(this.vaultId)}${parentOrigin ? `&parentOrigin=${encodeURIComponent(parentOrigin)}` : ''}`;
1415
1667
  document.body.appendChild(iframe);
1416
1668
  this.tokenizerFrame = iframe;
1669
+ this.log('mounting tokenizer iframe');
1417
1670
  };
1418
1671
  if (document.readyState === 'loading') {
1419
1672
  this._pendingMount = mount;
@@ -1449,6 +1702,12 @@
1449
1702
  // the previous session and justCompleted never fires, breaking auto-advance.
1450
1703
  if (msg.type === 'OZ_FRAME_READY') {
1451
1704
  this.completionState.set(frameId, false);
1705
+ if (msg.__ozVersion !== PROTOCOL_VERSION) {
1706
+ console.warn(`[OzVault] Protocol version mismatch on element frame "${frameId}" — ` +
1707
+ `SDK expects v${PROTOCOL_VERSION}, frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
1708
+ 'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
1709
+ }
1710
+ this.log('element iframe ready', { type: el.type, frameIdPrefix: frameId.slice(0, 8) });
1452
1711
  }
1453
1712
  // Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
1454
1713
  if (msg.type === 'OZ_CHANGE') {
@@ -1472,6 +1731,7 @@
1472
1731
  // Require valid too — avoids advancing at 13 digits for unknown-brand cards
1473
1732
  // where isComplete() fires before the user has finished typing.
1474
1733
  const justCompleted = complete && valid && !wasComplete;
1734
+ this.log('field changed', { type: el.type, complete, valid, justCompleted });
1475
1735
  // Sync CVV length when card brand changes
1476
1736
  if (el.type === 'cardNumber') {
1477
1737
  const brand = msg.cardBrand;
@@ -1483,17 +1743,25 @@
1483
1743
  // Auto-advance focus on completion
1484
1744
  if (justCompleted) {
1485
1745
  if (el.type === 'cardNumber') {
1746
+ this.log('auto-advance', { from: 'cardNumber', to: 'expirationDate' });
1486
1747
  (_b = this.elementsByType.get('expirationDate')) === null || _b === void 0 ? void 0 : _b.focus();
1487
1748
  }
1488
1749
  else if (el.type === 'expirationDate') {
1750
+ this.log('auto-advance', { from: 'expirationDate', to: 'cvv' });
1489
1751
  (_c = this.elementsByType.get('cvv')) === null || _c === void 0 ? void 0 : _c.focus();
1490
1752
  }
1491
1753
  }
1492
1754
  }
1493
1755
  handleTokenizerMessage(msg) {
1494
- var _a, _b, _c;
1756
+ var _a, _b, _c, _d;
1495
1757
  switch (msg.type) {
1496
1758
  case 'OZ_FRAME_READY':
1759
+ if (msg.__ozVersion !== PROTOCOL_VERSION) {
1760
+ console.warn(`[OzVault] Protocol version mismatch — SDK expects v${PROTOCOL_VERSION}, ` +
1761
+ `tokenizer frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
1762
+ 'This usually means the deployed frame files are stale. ' +
1763
+ 'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
1764
+ }
1497
1765
  this.tokenizerReady = true;
1498
1766
  if (this.loadErrorTimeoutId != null) {
1499
1767
  clearTimeout(this.loadErrorTimeoutId);
@@ -1505,6 +1773,7 @@
1505
1773
  // sent again from create() once the key is available.
1506
1774
  this.sendToTokenizer(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})));
1507
1775
  (_c = this._onReady) === null || _c === void 0 ? void 0 : _c.call(this);
1776
+ this.log('tokenizer iframe ready', { protocolVersion: (_d = msg.__ozVersion) !== null && _d !== void 0 ? _d : null });
1508
1777
  break;
1509
1778
  case 'OZ_TOKEN_RESULT': {
1510
1779
  if (typeof msg.requestId !== 'string' || !msg.requestId) {
@@ -1529,11 +1798,18 @@
1529
1798
  }
1530
1799
  pending.resolve(Object.assign(Object.assign({ token,
1531
1800
  cvcSession }, (card ? { card } : {})), (pending.billing ? { billing: pending.billing } : {})));
1801
+ this.log('token received', {
1802
+ elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
1803
+ tokenPresent: true,
1804
+ cvcSessionPresent: true,
1805
+ cardMetadataPresent: Boolean(card),
1806
+ });
1532
1807
  // Increment the per-key success counter and proactively refresh once
1533
1808
  // the budget is exhausted so the next createToken() call uses a fresh
1534
1809
  // key without waiting for a vault rejection.
1535
1810
  this._tokenizeSuccessCount++;
1536
1811
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1812
+ this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1537
1813
  this.refreshWaxKey().catch((err) => {
1538
1814
  console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1539
1815
  });
@@ -1553,14 +1829,25 @@
1553
1829
  this.tokenizeResolvers.delete(msg.requestId);
1554
1830
  if (pending.timeoutId != null)
1555
1831
  clearTimeout(pending.timeoutId);
1832
+ const willRefresh = this.isRefreshableAuthError(errorCode, raw) && !pending.retried && Boolean(this._storedFetchWaxKey);
1833
+ this.log('token error', { errorCode, willRefresh });
1556
1834
  // Auto-refresh: if the wax key expired or was consumed and we haven't
1557
1835
  // already retried for this request, transparently re-mint and retry.
1558
- if (this.isRefreshableAuthError(errorCode, raw) && !pending.retried && this._storedFetchWaxKey) {
1836
+ if (willRefresh) {
1837
+ const resetCountAtRetry = this._resetCount;
1559
1838
  this.refreshWaxKey().then(() => {
1560
1839
  if (this._destroyed) {
1561
1840
  pending.reject(new OzError('Vault destroyed during wax key refresh.'));
1562
1841
  return;
1563
1842
  }
1843
+ if (this._resetCount !== resetCountAtRetry) {
1844
+ // reset() was called while the wax key was refreshing — the fields
1845
+ // have been cleared and _tokenizing was zeroed. Reject the original
1846
+ // promise so it doesn't stay pending, and bail out without starting
1847
+ // a new retry (which would tokenize against empty fields).
1848
+ pending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1849
+ return;
1850
+ }
1564
1851
  const newRequestId = `req-${uuid()}`;
1565
1852
  // _tokenizing is still 'card' (cleanup() hasn't been called yet)
1566
1853
  this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
@@ -1607,11 +1894,16 @@
1607
1894
  if (bankPending.timeoutId != null)
1608
1895
  clearTimeout(bankPending.timeoutId);
1609
1896
  if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
1897
+ const resetCountAtRetry = this._resetCount;
1610
1898
  this.refreshWaxKey().then(() => {
1611
1899
  if (this._destroyed) {
1612
1900
  bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
1613
1901
  return;
1614
1902
  }
1903
+ if (this._resetCount !== resetCountAtRetry) {
1904
+ bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1905
+ return;
1906
+ }
1615
1907
  const newRequestId = `req-${uuid()}`;
1616
1908
  this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
1617
1909
  try {
@@ -1669,9 +1961,15 @@
1669
1961
  }
1670
1962
  const bank = isBankAccountMetadata(msg.bank) ? msg.bank : undefined;
1671
1963
  pending.resolve(Object.assign({ token }, (bank ? { bank } : {})));
1964
+ this.log('bank token received', {
1965
+ elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
1966
+ tokenPresent: true,
1967
+ bankMetadataPresent: Boolean(bank),
1968
+ });
1672
1969
  // Same proactive refresh logic as card tokenization.
1673
1970
  this._tokenizeSuccessCount++;
1674
1971
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1972
+ this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1675
1973
  this.refreshWaxKey().catch((err) => {
1676
1974
  console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1677
1975
  });
@@ -1727,6 +2025,7 @@
1727
2025
  }
1728
2026
  const newSessionId = uuid();
1729
2027
  (_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
2028
+ this.log('wax key refresh started');
1730
2029
  this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
1731
2030
  .then(newWaxKey => {
1732
2031
  if (typeof newWaxKey !== 'string' || !newWaxKey.trim()) {
@@ -1740,6 +2039,11 @@
1740
2039
  if (!this._destroyed && this.tokenizerReady) {
1741
2040
  this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey });
1742
2041
  }
2042
+ this.log('wax key refresh succeeded');
2043
+ })
2044
+ .catch((err) => {
2045
+ this.log('wax key refresh failed', { error: err instanceof Error ? err.message : String(err) });
2046
+ throw err;
1743
2047
  })
1744
2048
  .finally(() => {
1745
2049
  this._waxRefreshing = null;
@@ -1753,88 +2057,11 @@
1753
2057
  }
1754
2058
  }
1755
2059
 
1756
- /**
1757
- * Creates a ready-to-use `fetchWaxKey` callback for `OzVault.create()` and `<OzElements>`.
1758
- *
1759
- * Calls your backend mint endpoint with `{ sessionId }` and returns the wax key string.
1760
- * Throws on non-OK responses or a missing `waxKey` field so the vault can surface the
1761
- * error through its normal error path.
1762
- *
1763
- * Each call enforces a 10-second per-attempt timeout. On a pure network-level
1764
- * failure (connection refused, DNS failure, etc.) the call is retried once after
1765
- * 750ms before throwing. HTTP errors (4xx/5xx) are never retried — they indicate
1766
- * an endpoint misconfiguration or an invalid key, not a transient failure.
1767
- *
1768
- * The mint endpoint is typically the one-line `createMintWaxHandler` / `createMintWaxMiddleware`
1769
- * from `@ozura/elements/server`.
1770
- *
1771
- * @param mintUrl - Absolute or relative URL of your wax-key mint endpoint, e.g. `'/api/mint-wax'`.
1772
- *
1773
- * @example
1774
- * // Vanilla JS
1775
- * const vault = await OzVault.create({
1776
- * pubKey: 'pk_live_...',
1777
- * fetchWaxKey: createFetchWaxKey('/api/mint-wax'),
1778
- * });
1779
- *
1780
- * @example
1781
- * // React
1782
- * <OzElements pubKey="pk_live_..." fetchWaxKey={createFetchWaxKey('/api/mint-wax')}>
1783
- */
1784
- function createFetchWaxKey(mintUrl) {
1785
- const TIMEOUT_MS = 10000;
1786
- // Each attempt gets its own AbortController so a timeout on attempt 1 does
1787
- // not bleed into the retry. Uses AbortController + setTimeout instead of
1788
- // AbortSignal.timeout() to support environments without that API.
1789
- const attemptFetch = (sessionId) => {
1790
- const controller = new AbortController();
1791
- const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
1792
- return fetch(mintUrl, {
1793
- method: 'POST',
1794
- headers: { 'Content-Type': 'application/json' },
1795
- body: JSON.stringify({ sessionId }),
1796
- signal: controller.signal,
1797
- }).finally(() => clearTimeout(timer));
1798
- };
1799
- return async (sessionId) => {
1800
- let res;
1801
- try {
1802
- res = await attemptFetch(sessionId);
1803
- }
1804
- catch (firstErr) {
1805
- // Abort/timeout should not be retried — the server received nothing or
1806
- // we already waited the full timeout duration.
1807
- if (firstErr instanceof Error && (firstErr.name === 'AbortError' || firstErr.name === 'TimeoutError')) {
1808
- throw new OzError(`Wax key mint timed out after ${TIMEOUT_MS / 1000}s (${mintUrl})`, undefined, 'timeout');
1809
- }
1810
- // Pure network error (offline, DNS, connection refused) — retry once
1811
- // after a short pause in case of a transient blip.
1812
- await new Promise(resolve => setTimeout(resolve, 750));
1813
- try {
1814
- res = await attemptFetch(sessionId);
1815
- }
1816
- catch (retryErr) {
1817
- const msg = retryErr instanceof Error ? retryErr.message : 'Network error';
1818
- throw new OzError(`Could not reach wax key mint endpoint (${mintUrl}): ${msg}`, undefined, 'network');
1819
- }
1820
- }
1821
- const data = await res.json().catch(() => ({}));
1822
- if (!res.ok) {
1823
- throw new OzError(typeof data.error === 'string' && data.error
1824
- ? data.error
1825
- : `Wax key mint failed (HTTP ${res.status})`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
1826
- }
1827
- if (typeof data.waxKey !== 'string' || !data.waxKey.trim()) {
1828
- throw new OzError('Mint endpoint response is missing waxKey. Check your /api/mint-wax implementation.', undefined, 'validation');
1829
- }
1830
- return data.waxKey;
1831
- };
1832
- }
1833
-
1834
2060
  exports.OzElement = OzElement;
1835
2061
  exports.OzError = OzError;
1836
2062
  exports.OzVault = OzVault;
1837
- exports.createFetchWaxKey = createFetchWaxKey;
2063
+ exports.createFetchWaxKey = createSessionFetcher;
2064
+ exports.createSessionFetcher = createSessionFetcher;
1838
2065
  exports.normalizeBankVaultError = normalizeBankVaultError;
1839
2066
  exports.normalizeCardSaleError = normalizeCardSaleError;
1840
2067
  exports.normalizeVaultError = normalizeVaultError;