@ozura/elements 1.2.0 → 1.2.3

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 (60) hide show
  1. package/README.md +105 -0
  2. package/dist/frame/element-frame.js +60 -4
  3. package/dist/frame/element-frame.js.map +1 -1
  4. package/dist/frame/tokenizer-frame.js +145 -61
  5. package/dist/frame/tokenizer-frame.js.map +1 -1
  6. package/dist/oz-elements.esm.js +221 -97
  7. package/dist/oz-elements.esm.js.map +1 -1
  8. package/dist/oz-elements.umd.js +221 -97
  9. package/dist/oz-elements.umd.js.map +1 -1
  10. package/dist/react/frame/elementFrame.d.ts +9 -0
  11. package/dist/react/frame/tokenizerFrame.d.ts +17 -1
  12. package/dist/react/index.cjs.js +221 -97
  13. package/dist/react/index.cjs.js.map +1 -1
  14. package/dist/react/index.esm.js +221 -97
  15. package/dist/react/index.esm.js.map +1 -1
  16. package/dist/react/sdk/OzElement.d.ts +4 -1
  17. package/dist/react/sdk/OzVault.d.ts +3 -9
  18. package/dist/react/sdk/createSessionFetcher.d.ts +2 -2
  19. package/dist/react/server/index.d.ts +26 -0
  20. package/dist/react/types/index.d.ts +37 -14
  21. package/dist/react/utils/billingUtils.d.ts +2 -1
  22. package/dist/react/vue/index.d.ts +88 -0
  23. package/dist/server/frame/elementFrame.d.ts +9 -0
  24. package/dist/server/frame/tokenizerFrame.d.ts +17 -1
  25. package/dist/server/index.cjs.js +77 -27
  26. package/dist/server/index.cjs.js.map +1 -1
  27. package/dist/server/index.esm.js +77 -27
  28. package/dist/server/index.esm.js.map +1 -1
  29. package/dist/server/sdk/OzElement.d.ts +4 -1
  30. package/dist/server/sdk/OzVault.d.ts +3 -9
  31. package/dist/server/sdk/createSessionFetcher.d.ts +2 -2
  32. package/dist/server/server/index.d.ts +26 -0
  33. package/dist/server/types/index.d.ts +37 -14
  34. package/dist/server/utils/billingUtils.d.ts +2 -1
  35. package/dist/server/vue/index.d.ts +88 -0
  36. package/dist/types/frame/elementFrame.d.ts +9 -0
  37. package/dist/types/frame/tokenizerFrame.d.ts +17 -1
  38. package/dist/types/sdk/OzElement.d.ts +4 -1
  39. package/dist/types/sdk/OzVault.d.ts +3 -9
  40. package/dist/types/sdk/createSessionFetcher.d.ts +2 -2
  41. package/dist/types/server/index.d.ts +26 -0
  42. package/dist/types/types/index.d.ts +37 -14
  43. package/dist/types/utils/billingUtils.d.ts +2 -1
  44. package/dist/types/vue/index.d.ts +88 -0
  45. package/dist/vue/frame/protocol.d.ts +12 -0
  46. package/dist/vue/index.cjs.js +2335 -0
  47. package/dist/vue/index.cjs.js.map +1 -0
  48. package/dist/vue/index.esm.js +2327 -0
  49. package/dist/vue/index.esm.js.map +1 -0
  50. package/dist/vue/sdk/OzElement.d.ts +99 -0
  51. package/dist/vue/sdk/OzVault.d.ts +250 -0
  52. package/dist/vue/sdk/createSessionFetcher.d.ts +29 -0
  53. package/dist/vue/sdk/errors.d.ts +65 -0
  54. package/dist/vue/sdk/index.d.ts +14 -0
  55. package/dist/vue/types/index.d.ts +667 -0
  56. package/dist/vue/utils/appearance.d.ts +22 -0
  57. package/dist/vue/utils/billingUtils.d.ts +61 -0
  58. package/dist/vue/utils/uuid.d.ts +12 -0
  59. package/dist/vue/vue/index.d.ts +88 -0
  60. package/package.json +15 -3
@@ -198,7 +198,7 @@ function normalizeCommonVaultError(msg) {
198
198
  if (msg.includes('timeout') || msg.includes('timed out')) {
199
199
  return 'The request timed out. Please try again.';
200
200
  }
201
- if (msg.includes('http 5') || msg.includes('500') || msg.includes('502') || msg.includes('503')) {
201
+ if (msg.includes('http 5') || /\b5\d{2}\b/.test(msg)) {
202
202
  return 'A server error occurred. Please try again shortly.';
203
203
  }
204
204
  return null;
@@ -218,7 +218,7 @@ function normalizeVaultError(raw) {
218
218
  if (msg.includes('cvv') || msg.includes('cvc') || msg.includes('security code')) {
219
219
  return 'The CVV code is invalid. Please check and try again.';
220
220
  }
221
- if (msg.includes('insufficient') || msg.includes('funds')) {
221
+ if (msg.includes('insufficient funds')) {
222
222
  return 'Your card has insufficient funds. Please use a different card.';
223
223
  }
224
224
  if (msg.includes('declined') || msg.includes('do not honor')) {
@@ -376,7 +376,7 @@ function sanitizeOptions(options) {
376
376
  * it never holds raw card data — all sensitive values live in the iframe.
377
377
  */
378
378
  class OzElement {
379
- constructor(elementType, options, vaultId, frameBaseUrl, fonts = [], appearanceStyle) {
379
+ constructor(elementType, options, vaultId, frameBaseUrl, fonts = [], appearanceStyle, onDestroy, debug = false) {
380
380
  this.iframe = null;
381
381
  this._frameWindow = null;
382
382
  this._ready = false;
@@ -384,6 +384,7 @@ class OzElement {
384
384
  this._loadTimer = null;
385
385
  this.pendingMessages = [];
386
386
  this.handlers = new Map();
387
+ this.debug = false;
387
388
  this.elementType = elementType;
388
389
  this.options = sanitizeOptions(options);
389
390
  this.vaultId = vaultId;
@@ -392,6 +393,8 @@ class OzElement {
392
393
  this.fonts = fonts;
393
394
  this.appearanceStyle = appearanceStyle;
394
395
  this.frameId = `oz-${elementType}-${uuid()}`;
396
+ this._onDestroy = onDestroy;
397
+ this.debug = debug;
395
398
  }
396
399
  /** The element type this proxy represents. */
397
400
  get type() {
@@ -431,11 +434,17 @@ class OzElement {
431
434
  accountNumber: 'account number',
432
435
  routingNumber: 'routing number',
433
436
  }[this.elementType]) !== null && _a !== void 0 ? _a : this.elementType} input`;
434
- // Note: the `sandbox` attribute is intentionally NOT set. Field values are
435
- // delivered to the tokenizer iframe via a MessageChannel port (transferred
436
- // in OZ_BEGIN_COLLECT), so no window.parent named-frame lookup is needed.
437
- // The security boundary is the vault URL hardcoded at build time and the
438
- // origin checks on every postMessage, not the sandbox flag.
437
+ // sandbox="allow-scripts" gives correct iframe isolation:
438
+ // - Scripts run (allow-scripts), so the field JS executes normally.
439
+ // - NO allow-same-origin: the frame cannot access window.parent's DOM,
440
+ // localStorage, or cookies prevents sandbox escape even if served
441
+ // from the same origin.
442
+ // - NO allow-top-navigation: a rogue/compromised element frame cannot
443
+ // navigate window.top (clickjacking prevention).
444
+ // - NO allow-forms / allow-popups: reduces attack surface.
445
+ // Field values are delivered via postMessage, so no parent access is
446
+ // needed — allow-scripts alone is sufficient.
447
+ iframe.setAttribute('sandbox', 'allow-scripts');
439
448
  // Use hash instead of query string — survives clean-URL redirects from static servers.
440
449
  // parentOrigin lets the frame target postMessage to the merchant origin instead of '*'.
441
450
  const parentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
@@ -547,9 +556,14 @@ class OzElement {
547
556
  * and prevents future use. Distinct from `unmount()` which allows re-mounting.
548
557
  */
549
558
  destroy() {
559
+ var _a;
550
560
  this.unmount();
551
561
  this.handlers.clear();
552
562
  this._destroyed = true;
563
+ // Notify OzVault so it can prune the stale frameId entry from its elements
564
+ // and completionState maps. Without this, manually calling el.destroy() leaks
565
+ // map entries that grow unboundedly in SPA scenarios with repeated mount/unmount.
566
+ (_a = this._onDestroy) === null || _a === void 0 ? void 0 : _a.call(this);
553
567
  }
554
568
  // ─── Called by OzVault ───────────────────────────────────────────────────
555
569
  /**
@@ -586,7 +600,7 @@ class OzElement {
586
600
  }
587
601
  this._frameWindow = (_b = (_a = this.iframe) === null || _a === void 0 ? void 0 : _a.contentWindow) !== null && _b !== void 0 ? _b : null;
588
602
  const mergedOptions = Object.assign(Object.assign({}, this.options), { style: mergeAppearanceWithElementStyle(this.appearanceStyle, this.options.style) });
589
- this.post(Object.assign({ type: 'OZ_INIT', elementType: this.elementType, options: sanitizeOptions(mergedOptions), frameId: this.frameId }, (this.fonts.length > 0 ? { fonts: this.fonts } : {})));
603
+ this.post(Object.assign({ type: 'OZ_INIT', elementType: this.elementType, options: sanitizeOptions(mergedOptions), frameId: this.frameId, debug: this.debug }, (this.fonts.length > 0 ? { fonts: this.fonts } : {})));
590
604
  this.pendingMessages.forEach(m => this.send(m));
591
605
  this.pendingMessages = [];
592
606
  this.emit('ready', undefined);
@@ -700,13 +714,14 @@ function validateEmail(email) {
700
714
  // ─── Phone ───────────────────────────────────────────────────────────────────
701
715
  /**
702
716
  * Validates E.164 phone format: starts with +, 1–3 digit country code,
703
- * followed by 7–12 digits, total ≤50 characters.
717
+ * followed by 7–12 digits, max 15 digits total (E.164 spec cap = 16 chars
718
+ * including the leading +).
704
719
  *
705
720
  * Matches the output of checkout's formatPhoneForAPI() function.
706
721
  * Examples: "+15551234567", "+447911123456", "+61412345678"
707
722
  */
708
723
  function validateE164Phone(phone) {
709
- return /^\+[1-9]\d{6,49}$/.test(phone) && phone.length <= 50;
724
+ return /^\+[1-9]\d{6,14}$/.test(phone);
710
725
  }
711
726
  // ─── Field length ─────────────────────────────────────────────────────────────
712
727
  /** Returns true when the string is non-empty and ≤50 characters (cardSale schema). */
@@ -909,12 +924,12 @@ const PROTOCOL_VERSION = 1;
909
924
  *
910
925
  * @example
911
926
  * // Simplest — just pass sessionUrl, no need to call this directly
912
- * const vault = await OzVault.create({ pubKey: 'pk_live_...', sessionUrl: '/api/oz-session' });
927
+ * const vault = await OzVault.create({ pubKey: 'pk_prod_...', sessionUrl: '/api/oz-session' });
913
928
  *
914
929
  * @example
915
930
  * // Manual — use when you need custom headers
916
931
  * const vault = await OzVault.create({
917
- * pubKey: 'pk_live_...',
932
+ * pubKey: 'pk_prod_...',
918
933
  * getSessionKey: createSessionFetcher('/api/oz-session'),
919
934
  * });
920
935
  */
@@ -952,7 +967,22 @@ function createSessionFetcher(url) {
952
967
  throw new OzError(`Could not reach session endpoint (${url}): ${msg}`, undefined, 'network');
953
968
  }
954
969
  }
955
- const data = await res.json().catch(() => ({}));
970
+ // Parse JSON separately from the ok-check so that a non-JSON error body
971
+ // (HTML error page, WAF block, CDN 503) produces the right error code.
972
+ // Previously res.json() was attempted before res.ok was checked; a parse
973
+ // failure on a 5xx HTML body would fall through as {} and produce a
974
+ // misleading 'validation' code when the real cause is a server/network issue.
975
+ let data = {};
976
+ try {
977
+ data = await res.json();
978
+ }
979
+ catch (_a) {
980
+ if (!res.ok) {
981
+ throw new OzError(`Session endpoint returned HTTP ${res.status} with a non-JSON body`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
982
+ }
983
+ // HTTP 200 but body isn't JSON — this is a misconfigured session endpoint.
984
+ throw new OzError('Session endpoint returned HTTP 200 but the response body is not valid JSON. Check your /api/oz-session implementation.', undefined, 'validation');
985
+ }
956
986
  if (!res.ok) {
957
987
  throw new OzError(typeof data.error === 'string' && data.error
958
988
  ? data.error
@@ -970,10 +1000,19 @@ function createSessionFetcher(url) {
970
1000
  }
971
1001
 
972
1002
  function isCardMetadata(v) {
973
- return !!v && typeof v === 'object' && typeof v.last4 === 'string';
1003
+ if (!v || typeof v !== 'object')
1004
+ return false;
1005
+ const r = v;
1006
+ return (typeof r.last4 === 'string' &&
1007
+ typeof r.brand === 'string' &&
1008
+ typeof r.expMonth === 'string' &&
1009
+ typeof r.expYear === 'string');
974
1010
  }
975
1011
  function isBankAccountMetadata(v) {
976
- return !!v && typeof v === 'object' && typeof v.last4 === 'string';
1012
+ if (!v || typeof v !== 'object')
1013
+ return false;
1014
+ const r = v;
1015
+ return typeof r.last4 === 'string' && typeof r.routingNumberLast4 === 'string';
977
1016
  }
978
1017
  const DEFAULT_FRAME_BASE_URL = "https://elements.ozura.com";
979
1018
  /**
@@ -983,16 +1022,10 @@ const DEFAULT_FRAME_BASE_URL = "https://elements.ozura.com";
983
1022
  * Use the static `OzVault.create()` factory — do not call `new OzVault()` directly.
984
1023
  *
985
1024
  * @example
1025
+ * // Recommended — pass sessionUrl and let the SDK call your backend automatically
986
1026
  * const vault = await OzVault.create({
987
- * pubKey: 'pk_live_...',
988
- * fetchWaxKey: async (sessionId) => {
989
- * // Call your backend — which calls ozura.mintWaxKey() from @ozura/elements/server
990
- * const { waxKey } = await fetch('/api/mint-wax', {
991
- * method: 'POST',
992
- * body: JSON.stringify({ sessionId }),
993
- * }).then(r => r.json());
994
- * return waxKey;
995
- * },
1027
+ * pubKey: 'pk_prod_...', // or 'pk_test_...' for test mode
1028
+ * sessionUrl: '/api/oz-session', // backend endpoint that calls ozura.createSession()
996
1029
  * });
997
1030
  * const cardNum = vault.createElement('cardNumber');
998
1031
  * cardNum.mount('#card-number');
@@ -1038,6 +1071,11 @@ class OzVault {
1038
1071
  this.tokenizationSessionId = tokenizationSessionId;
1039
1072
  this.pubKey = options.pubKey;
1040
1073
  this.frameBaseUrl = options.frameBaseUrl || DEFAULT_FRAME_BASE_URL;
1074
+ // Validate immediately after assignment
1075
+ if (!this.frameBaseUrl.startsWith('https://') &&
1076
+ !this.frameBaseUrl.startsWith('http://localhost')) {
1077
+ throw new OzError('frameBaseUrl must use HTTPS (http://localhost is allowed for local development)', undefined, 'config');
1078
+ }
1041
1079
  this.frameOrigin = new URL(this.frameBaseUrl).origin;
1042
1080
  this.fonts = (_a = options.fonts) !== null && _a !== void 0 ? _a : [];
1043
1081
  this.resolvedAppearance = resolveAppearance(options.appearance);
@@ -1146,7 +1184,7 @@ class OzVault {
1146
1184
  // the OZ_INIT sent at that point had an empty waxKey. Send a follow-up now
1147
1185
  // so the tokenizer has the key stored before any createToken() call.
1148
1186
  if (vault.tokenizerReady) {
1149
- vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey });
1187
+ vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey, debug: vault._debug });
1150
1188
  }
1151
1189
  vault.log('wax key received — vault ready');
1152
1190
  return vault;
@@ -1228,7 +1266,12 @@ class OzVault {
1228
1266
  this.completionState.delete(existing.frameId);
1229
1267
  existing.destroy();
1230
1268
  }
1231
- const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance);
1269
+ const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance, () => {
1270
+ // Prune vault-level maps when the element is manually destroyed so they
1271
+ // don't grow unboundedly in SPA scenarios with repeated mount/unmount cycles.
1272
+ this.elements.delete(el.frameId);
1273
+ this.completionState.delete(el.frameId);
1274
+ }, this._debug);
1232
1275
  this.elements.set(el.frameId, el);
1233
1276
  typeMap.set(type, el);
1234
1277
  return el;
@@ -1311,7 +1354,6 @@ class OzVault {
1311
1354
  this.sendToTokenizer({
1312
1355
  type: 'OZ_BANK_TOKENIZE',
1313
1356
  requestId,
1314
- waxKey: this.waxKey,
1315
1357
  tokenizationSessionId: this.tokenizationSessionId,
1316
1358
  pubKey: this.pubKey,
1317
1359
  firstName: options.firstName.trim(),
@@ -1319,7 +1361,10 @@ class OzVault {
1319
1361
  fieldCount: readyBankElements.length,
1320
1362
  }, bankChannels.map(ch => ch.port1));
1321
1363
  this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
1322
- readyBankElements.forEach((el, i) => el.beginCollect(requestId, bankChannels[i].port2));
1364
+ readyBankElements.forEach((el, i) => {
1365
+ this.log('OZ_BEGIN_COLLECT dispatched', { type: el.type, requestIdPrefix: `${requestId.slice(0, 12)}...` });
1366
+ el.beginCollect(requestId, bankChannels[i].port2);
1367
+ });
1323
1368
  const bankTimeoutId = setTimeout(() => {
1324
1369
  if (this.bankTokenizeResolvers.has(requestId)) {
1325
1370
  this.bankTokenizeResolvers.delete(requestId);
@@ -1403,7 +1448,11 @@ class OzVault {
1403
1448
  }
1404
1449
  this._tokenizing = 'card';
1405
1450
  const requestId = `req-${uuid()}`;
1406
- this.log('createToken() called');
1451
+ this.log('createToken() called', {
1452
+ requestIdPrefix: requestId.slice(0, 12),
1453
+ fields: readyElements.map(el => el.type),
1454
+ billingPresent: Boolean(options.billing),
1455
+ });
1407
1456
  return new Promise((resolve, reject) => {
1408
1457
  // Capture the reset generation so cleanup() only zeros _tokenizing when it
1409
1458
  // still belongs to this invocation — not a newer one that started after a reset.
@@ -1428,7 +1477,6 @@ class OzVault {
1428
1477
  this.sendToTokenizer({
1429
1478
  type: 'OZ_TOKENIZE',
1430
1479
  requestId,
1431
- waxKey: this.waxKey,
1432
1480
  tokenizationSessionId: this.tokenizationSessionId,
1433
1481
  pubKey: this.pubKey,
1434
1482
  firstName,
@@ -1441,7 +1489,10 @@ class OzVault {
1441
1489
  if (cardEntry)
1442
1490
  cardEntry.tokenizeStartMs = tokenizeStartMs;
1443
1491
  // Tell each ready element frame to send its raw value to the tokenizer
1444
- readyElements.forEach((el, i) => el.beginCollect(requestId, cardChannels[i].port2));
1492
+ readyElements.forEach((el, i) => {
1493
+ this.log('OZ_BEGIN_COLLECT dispatched', { type: el.type, requestIdPrefix: `${requestId.slice(0, 12)}...` });
1494
+ el.beginCollect(requestId, cardChannels[i].port2);
1495
+ });
1445
1496
  const cardTimeoutId = setTimeout(() => {
1446
1497
  if (this.tokenizeResolvers.has(requestId)) {
1447
1498
  this.tokenizeResolvers.delete(requestId);
@@ -1494,6 +1545,7 @@ class OzVault {
1494
1545
  this.tokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1495
1546
  if (timeoutId != null)
1496
1547
  clearTimeout(timeoutId);
1548
+ this.log('OZ_TOKENIZE_CANCEL sent (reset)', { requestIdPrefix: `${requestId.slice(0, 12)}...` });
1497
1549
  this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1498
1550
  reject(new OzError('Vault was reset while tokenization was in progress.'));
1499
1551
  });
@@ -1501,6 +1553,7 @@ class OzVault {
1501
1553
  this.bankTokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1502
1554
  if (timeoutId != null)
1503
1555
  clearTimeout(timeoutId);
1556
+ this.log('OZ_TOKENIZE_CANCEL sent (reset, bank)', { requestIdPrefix: `${requestId.slice(0, 12)}...` });
1504
1557
  this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1505
1558
  reject(new OzError('Vault was reset while tokenization was in progress.'));
1506
1559
  });
@@ -1675,7 +1728,7 @@ class OzVault {
1675
1728
  }
1676
1729
  }
1677
1730
  handleMessage(event) {
1678
- var _a;
1731
+ var _a, _b;
1679
1732
  if (this._destroyed)
1680
1733
  return;
1681
1734
  // Only accept messages from our frame origin (defense in depth; prevents
@@ -1690,6 +1743,15 @@ class OzVault {
1690
1743
  this.handleTokenizerMessage(msg);
1691
1744
  return;
1692
1745
  }
1746
+ // OZ_TOKEN_ERROR can arrive from element frames when the MessagePort
1747
+ // transferred in OZ_BEGIN_COLLECT was dropped by the browser (e.g. the
1748
+ // frame navigated). These carry a requestId but no frameId — route them
1749
+ // through handleTokenizerMessage so the pending promise is rejected
1750
+ // immediately rather than waiting for the 30 s collect timeout.
1751
+ if (msg.type === 'OZ_TOKEN_ERROR') {
1752
+ this.handleTokenizerMessage(msg);
1753
+ return;
1754
+ }
1693
1755
  // Route to the matching element
1694
1756
  const frameId = msg.frameId;
1695
1757
  if (frameId) {
@@ -1707,6 +1769,23 @@ class OzVault {
1707
1769
  }
1708
1770
  this.log('element iframe ready', { type: el.type, frameIdPrefix: frameId.slice(0, 8) });
1709
1771
  }
1772
+ // Relay debug/warning messages from element iframes into the parent
1773
+ // DevTools console. Element frames run in cross-origin iframes whose
1774
+ // console context is invisible to developers without a frame selector switch.
1775
+ // Errors are always surfaced (genuine failures). Warnings are only emitted
1776
+ // when debug mode is on — CSS var() and font-host warnings fire on every
1777
+ // style update and would pollute production consoles otherwise.
1778
+ if (msg.type === 'OZ_DEBUG_LOG') {
1779
+ const level = typeof msg.level === 'string' ? msg.level : 'warn';
1780
+ const message = typeof msg.message === 'string' ? msg.message : String((_b = msg.message) !== null && _b !== void 0 ? _b : '');
1781
+ if (level === 'error') {
1782
+ console.error(`[OzVault:${el.type}] ${message}`);
1783
+ }
1784
+ else if (this._debug) {
1785
+ console.warn(`[OzVault:${el.type}] ${message}`);
1786
+ }
1787
+ return;
1788
+ }
1710
1789
  // Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
1711
1790
  if (msg.type === 'OZ_CHANGE') {
1712
1791
  this.handleElementChange(msg, el);
@@ -1751,7 +1830,7 @@ class OzVault {
1751
1830
  }
1752
1831
  }
1753
1832
  handleTokenizerMessage(msg) {
1754
- var _a, _b, _c, _d;
1833
+ var _a, _b, _c, _d, _e;
1755
1834
  switch (msg.type) {
1756
1835
  case 'OZ_FRAME_READY':
1757
1836
  if (msg.__ozVersion !== PROTOCOL_VERSION) {
@@ -1769,9 +1848,10 @@ class OzVault {
1769
1848
  // Deliver the wax key via OZ_INIT so the tokenizer stores it internally.
1770
1849
  // If waxKey is still empty (fetchWaxKey hasn't resolved yet), it will be
1771
1850
  // sent again from create() once the key is available.
1772
- this.sendToTokenizer(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})));
1851
+ this.sendToTokenizer(Object.assign(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})), { debug: this._debug }));
1773
1852
  (_c = this._onReady) === null || _c === void 0 ? void 0 : _c.call(this);
1774
1853
  this.log('tokenizer iframe ready', { protocolVersion: (_d = msg.__ozVersion) !== null && _d !== void 0 ? _d : null });
1854
+ this.log('vault state', this.debugState());
1775
1855
  break;
1776
1856
  case 'OZ_TOKEN_RESULT': {
1777
1857
  if (typeof msg.requestId !== 'string' || !msg.requestId) {
@@ -1797,6 +1877,7 @@ class OzVault {
1797
1877
  pending.resolve(Object.assign(Object.assign({ token,
1798
1878
  cvcSession }, (card ? { card } : {})), (pending.billing ? { billing: pending.billing } : {})));
1799
1879
  this.log('token received', {
1880
+ requestIdPrefix: msg.requestId.slice(0, 12),
1800
1881
  elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
1801
1882
  tokenPresent: true,
1802
1883
  cvcSessionPresent: true,
@@ -1828,7 +1909,7 @@ class OzVault {
1828
1909
  if (pending.timeoutId != null)
1829
1910
  clearTimeout(pending.timeoutId);
1830
1911
  const willRefresh = this.isRefreshableAuthError(errorCode, raw) && !pending.retried && Boolean(this._storedFetchWaxKey);
1831
- this.log('token error', { errorCode, willRefresh });
1912
+ this.log('token error', { requestIdPrefix: msg.requestId.slice(0, 12), errorCode, willRefresh });
1832
1913
  // Auto-refresh: if the wax key expired or was consumed and we haven't
1833
1914
  // already retried for this request, transparently re-mint and retry.
1834
1915
  if (willRefresh) {
@@ -1846,6 +1927,17 @@ class OzVault {
1846
1927
  pending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1847
1928
  return;
1848
1929
  }
1930
+ // Verify all elements from the original call are still mounted and
1931
+ // ready. If any were destroyed (e.g. the merchant called
1932
+ // createElement() or destroy() during the async refresh), the
1933
+ // beginCollect() calls below would silently no-op — the tokenizer
1934
+ // would wait forever for field values that never arrive. Reject
1935
+ // immediately with a clear message instead of hanging 30 seconds.
1936
+ const allCardElementsStillReady = pending.readyElements.every(el => this.elements.has(el.frameId) && el.isReady);
1937
+ if (!allCardElementsStillReady) {
1938
+ pending.reject(new OzError('Card fields changed during session refresh. Please re-enter your card details.'));
1939
+ return;
1940
+ }
1849
1941
  const newRequestId = `req-${uuid()}`;
1850
1942
  // _tokenizing is still 'card' (cleanup() hasn't been called yet)
1851
1943
  this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
@@ -1854,14 +1946,16 @@ class OzVault {
1854
1946
  this.sendToTokenizer({
1855
1947
  type: 'OZ_TOKENIZE',
1856
1948
  requestId: newRequestId,
1857
- waxKey: this.waxKey,
1858
1949
  tokenizationSessionId: this.tokenizationSessionId,
1859
1950
  pubKey: this.pubKey,
1860
1951
  firstName: pending.firstName,
1861
1952
  lastName: pending.lastName,
1862
1953
  fieldCount: pending.fieldCount,
1863
1954
  }, retryCardChannels.map(ch => ch.port1));
1864
- pending.readyElements.forEach((el, i) => el.beginCollect(newRequestId, retryCardChannels[i].port2));
1955
+ pending.readyElements.forEach((el, i) => {
1956
+ this.log('OZ_BEGIN_COLLECT dispatched (retry)', { type: el.type, requestIdPrefix: `${newRequestId.slice(0, 12)}...` });
1957
+ el.beginCollect(newRequestId, retryCardChannels[i].port2);
1958
+ });
1865
1959
  const retryCardTimeoutId = setTimeout(() => {
1866
1960
  if (this.tokenizeResolvers.has(newRequestId)) {
1867
1961
  this.tokenizeResolvers.delete(newRequestId);
@@ -1878,68 +1972,80 @@ class OzVault {
1878
1972
  pending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry tokenization failed to start'));
1879
1973
  }
1880
1974
  }).catch((refreshErr) => {
1881
- const msg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
1882
- pending.reject(new OzError(msg, undefined, 'auth'));
1975
+ const errMsg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
1976
+ pending.reject(new OzError(errMsg, undefined, 'auth'));
1883
1977
  });
1884
1978
  break;
1885
1979
  }
1886
1980
  pending.reject(new OzError(normalizeVaultError(raw), raw, errorCode));
1887
1981
  }
1888
- // Also check bank resolvers — both card and bank errors use OZ_TOKEN_ERROR
1889
- const bankPending = this.bankTokenizeResolvers.get(msg.requestId);
1890
- if (bankPending) {
1891
- this.bankTokenizeResolvers.delete(msg.requestId);
1892
- if (bankPending.timeoutId != null)
1893
- clearTimeout(bankPending.timeoutId);
1894
- if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
1895
- const resetCountAtRetry = this._resetCount;
1896
- this.refreshWaxKey().then(() => {
1897
- if (this._destroyed) {
1898
- bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
1899
- return;
1900
- }
1901
- if (this._resetCount !== resetCountAtRetry) {
1902
- bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1903
- return;
1904
- }
1905
- const newRequestId = `req-${uuid()}`;
1906
- this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
1907
- try {
1908
- const retryBankChannels = bankPending.readyElements.map(() => new MessageChannel());
1909
- this.sendToTokenizer({
1910
- type: 'OZ_BANK_TOKENIZE',
1911
- requestId: newRequestId,
1912
- waxKey: this.waxKey,
1913
- tokenizationSessionId: this.tokenizationSessionId,
1914
- pubKey: this.pubKey,
1915
- firstName: bankPending.firstName,
1916
- lastName: bankPending.lastName,
1917
- fieldCount: bankPending.fieldCount,
1918
- }, retryBankChannels.map(ch => ch.port1));
1919
- bankPending.readyElements.forEach((el, i) => el.beginCollect(newRequestId, retryBankChannels[i].port2));
1920
- const retryBankTimeoutId = setTimeout(() => {
1921
- if (this.bankTokenizeResolvers.has(newRequestId)) {
1922
- this.bankTokenizeResolvers.delete(newRequestId);
1923
- this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId: newRequestId });
1924
- bankPending.reject(new OzError('Bank tokenization timed out after wax key refresh.', undefined, 'timeout'));
1925
- }
1926
- }, 30000);
1927
- const retryBankEntry = this.bankTokenizeResolvers.get(newRequestId);
1928
- if (retryBankEntry)
1929
- retryBankEntry.timeoutId = retryBankTimeoutId;
1930
- }
1931
- catch (setupErr) {
1932
- this.bankTokenizeResolvers.delete(newRequestId);
1933
- bankPending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry bank tokenization failed to start'));
1934
- }
1935
- }).catch((refreshErr) => {
1936
- const msg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
1937
- bankPending.reject(new OzError(msg, undefined, 'auth'));
1938
- });
1939
- break;
1982
+ else {
1983
+ // Also check bank resolvers — both card and bank errors use OZ_TOKEN_ERROR.
1984
+ // Use else-if rather than sequential checks so a UUID collision (however
1985
+ // improbable) can never trigger double-rejection of two unrelated resolvers.
1986
+ const bankPending = this.bankTokenizeResolvers.get(msg.requestId);
1987
+ if (bankPending) {
1988
+ this.bankTokenizeResolvers.delete(msg.requestId);
1989
+ if (bankPending.timeoutId != null)
1990
+ clearTimeout(bankPending.timeoutId);
1991
+ if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
1992
+ const resetCountAtRetry = this._resetCount;
1993
+ this.refreshWaxKey().then(() => {
1994
+ if (this._destroyed) {
1995
+ bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
1996
+ return;
1997
+ }
1998
+ if (this._resetCount !== resetCountAtRetry) {
1999
+ bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
2000
+ return;
2001
+ }
2002
+ // Same stale-element guard as the card retry path above.
2003
+ const allBankElementsStillReady = bankPending.readyElements.every(el => this.elements.has(el.frameId) && el.isReady);
2004
+ if (!allBankElementsStillReady) {
2005
+ bankPending.reject(new OzError('Bank fields changed during session refresh. Please re-enter your account details.'));
2006
+ return;
2007
+ }
2008
+ const newRequestId = `req-${uuid()}`;
2009
+ this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
2010
+ try {
2011
+ const retryBankChannels = bankPending.readyElements.map(() => new MessageChannel());
2012
+ this.sendToTokenizer({
2013
+ type: 'OZ_BANK_TOKENIZE',
2014
+ requestId: newRequestId,
2015
+ tokenizationSessionId: this.tokenizationSessionId,
2016
+ pubKey: this.pubKey,
2017
+ firstName: bankPending.firstName,
2018
+ lastName: bankPending.lastName,
2019
+ fieldCount: bankPending.fieldCount,
2020
+ }, retryBankChannels.map(ch => ch.port1));
2021
+ bankPending.readyElements.forEach((el, i) => {
2022
+ this.log('OZ_BEGIN_COLLECT dispatched (retry)', { type: el.type, requestIdPrefix: `${newRequestId.slice(0, 12)}...` });
2023
+ el.beginCollect(newRequestId, retryBankChannels[i].port2);
2024
+ });
2025
+ const retryBankTimeoutId = setTimeout(() => {
2026
+ if (this.bankTokenizeResolvers.has(newRequestId)) {
2027
+ this.bankTokenizeResolvers.delete(newRequestId);
2028
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId: newRequestId });
2029
+ bankPending.reject(new OzError('Bank tokenization timed out after wax key refresh.', undefined, 'timeout'));
2030
+ }
2031
+ }, 30000);
2032
+ const retryBankEntry = this.bankTokenizeResolvers.get(newRequestId);
2033
+ if (retryBankEntry)
2034
+ retryBankEntry.timeoutId = retryBankTimeoutId;
2035
+ }
2036
+ catch (setupErr) {
2037
+ this.bankTokenizeResolvers.delete(newRequestId);
2038
+ bankPending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry bank tokenization failed to start'));
2039
+ }
2040
+ }).catch((refreshErr) => {
2041
+ const errMsg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
2042
+ bankPending.reject(new OzError(errMsg, undefined, 'auth'));
2043
+ });
2044
+ break;
2045
+ }
2046
+ bankPending.reject(new OzError(normalizeBankVaultError(raw), raw, errorCode));
1940
2047
  }
1941
- bankPending.reject(new OzError(normalizeBankVaultError(raw), raw, errorCode));
1942
- }
2048
+ } // end else (bank path)
1943
2049
  break;
1944
2050
  }
1945
2051
  case 'OZ_BANK_TOKEN_RESULT': {
@@ -1975,6 +2081,23 @@ class OzVault {
1975
2081
  }
1976
2082
  break;
1977
2083
  }
2084
+ case 'OZ_DEBUG_LOG': {
2085
+ // Relay warnings/errors from the tokenizer iframe into the parent page's
2086
+ // DevTools console. The tokenizer runs in a cross-origin iframe whose
2087
+ // console context is invisible to most developers unless they manually
2088
+ // switch the DevTools frame selector.
2089
+ // Errors are always surfaced; warnings only in debug mode to avoid
2090
+ // polluting production consoles.
2091
+ const level = typeof msg.level === 'string' ? msg.level : 'warn';
2092
+ const message = typeof msg.message === 'string' ? msg.message : String((_e = msg.message) !== null && _e !== void 0 ? _e : '');
2093
+ if (level === 'error') {
2094
+ console.error(`[OzVault:tokenizer] ${message}`);
2095
+ }
2096
+ else if (this._debug) {
2097
+ console.warn(`[OzVault:tokenizer] ${message}`);
2098
+ }
2099
+ break;
2100
+ }
1978
2101
  }
1979
2102
  }
1980
2103
  /**
@@ -2023,6 +2146,7 @@ class OzVault {
2023
2146
  }
2024
2147
  const newSessionId = uuid();
2025
2148
  (_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
2149
+ const refreshStartMs = Date.now();
2026
2150
  this.log('wax key refresh started');
2027
2151
  this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
2028
2152
  .then(newWaxKey => {
@@ -2035,12 +2159,12 @@ class OzVault {
2035
2159
  this._tokenizeSuccessCount = 0;
2036
2160
  }
2037
2161
  if (!this._destroyed && this.tokenizerReady) {
2038
- this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey });
2162
+ this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey, debug: this._debug });
2039
2163
  }
2040
- this.log('wax key refresh succeeded');
2164
+ this.log('wax key refresh succeeded', { durationMs: Date.now() - refreshStartMs });
2041
2165
  })
2042
2166
  .catch((err) => {
2043
- this.log('wax key refresh failed', { error: err instanceof Error ? err.message : String(err) });
2167
+ this.log('wax key refresh failed', { error: err instanceof Error ? err.message : String(err), durationMs: Date.now() - refreshStartMs });
2044
2168
  throw err;
2045
2169
  })
2046
2170
  .finally(() => {