@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
@@ -201,7 +201,7 @@ function normalizeCommonVaultError(msg) {
201
201
  if (msg.includes('timeout') || msg.includes('timed out')) {
202
202
  return 'The request timed out. Please try again.';
203
203
  }
204
- if (msg.includes('http 5') || msg.includes('500') || msg.includes('502') || msg.includes('503')) {
204
+ if (msg.includes('http 5') || /\b5\d{2}\b/.test(msg)) {
205
205
  return 'A server error occurred. Please try again shortly.';
206
206
  }
207
207
  return null;
@@ -221,7 +221,7 @@ function normalizeVaultError(raw) {
221
221
  if (msg.includes('cvv') || msg.includes('cvc') || msg.includes('security code')) {
222
222
  return 'The CVV code is invalid. Please check and try again.';
223
223
  }
224
- if (msg.includes('insufficient') || msg.includes('funds')) {
224
+ if (msg.includes('insufficient funds')) {
225
225
  return 'Your card has insufficient funds. Please use a different card.';
226
226
  }
227
227
  if (msg.includes('declined') || msg.includes('do not honor')) {
@@ -324,7 +324,7 @@ function sanitizeOptions(options) {
324
324
  * it never holds raw card data — all sensitive values live in the iframe.
325
325
  */
326
326
  class OzElement {
327
- constructor(elementType, options, vaultId, frameBaseUrl, fonts = [], appearanceStyle) {
327
+ constructor(elementType, options, vaultId, frameBaseUrl, fonts = [], appearanceStyle, onDestroy, debug = false) {
328
328
  this.iframe = null;
329
329
  this._frameWindow = null;
330
330
  this._ready = false;
@@ -332,6 +332,7 @@ class OzElement {
332
332
  this._loadTimer = null;
333
333
  this.pendingMessages = [];
334
334
  this.handlers = new Map();
335
+ this.debug = false;
335
336
  this.elementType = elementType;
336
337
  this.options = sanitizeOptions(options);
337
338
  this.vaultId = vaultId;
@@ -340,6 +341,8 @@ class OzElement {
340
341
  this.fonts = fonts;
341
342
  this.appearanceStyle = appearanceStyle;
342
343
  this.frameId = `oz-${elementType}-${uuid()}`;
344
+ this._onDestroy = onDestroy;
345
+ this.debug = debug;
343
346
  }
344
347
  /** The element type this proxy represents. */
345
348
  get type() {
@@ -379,11 +382,17 @@ class OzElement {
379
382
  accountNumber: 'account number',
380
383
  routingNumber: 'routing number',
381
384
  }[this.elementType]) !== null && _a !== void 0 ? _a : this.elementType} input`;
382
- // Note: the `sandbox` attribute is intentionally NOT set. Field values are
383
- // delivered to the tokenizer iframe via a MessageChannel port (transferred
384
- // in OZ_BEGIN_COLLECT), so no window.parent named-frame lookup is needed.
385
- // The security boundary is the vault URL hardcoded at build time and the
386
- // origin checks on every postMessage, not the sandbox flag.
385
+ // sandbox="allow-scripts" gives correct iframe isolation:
386
+ // - Scripts run (allow-scripts), so the field JS executes normally.
387
+ // - NO allow-same-origin: the frame cannot access window.parent's DOM,
388
+ // localStorage, or cookies prevents sandbox escape even if served
389
+ // from the same origin.
390
+ // - NO allow-top-navigation: a rogue/compromised element frame cannot
391
+ // navigate window.top (clickjacking prevention).
392
+ // - NO allow-forms / allow-popups: reduces attack surface.
393
+ // Field values are delivered via postMessage, so no parent access is
394
+ // needed — allow-scripts alone is sufficient.
395
+ iframe.setAttribute('sandbox', 'allow-scripts');
387
396
  // Use hash instead of query string — survives clean-URL redirects from static servers.
388
397
  // parentOrigin lets the frame target postMessage to the merchant origin instead of '*'.
389
398
  const parentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
@@ -495,9 +504,14 @@ class OzElement {
495
504
  * and prevents future use. Distinct from `unmount()` which allows re-mounting.
496
505
  */
497
506
  destroy() {
507
+ var _a;
498
508
  this.unmount();
499
509
  this.handlers.clear();
500
510
  this._destroyed = true;
511
+ // Notify OzVault so it can prune the stale frameId entry from its elements
512
+ // and completionState maps. Without this, manually calling el.destroy() leaks
513
+ // map entries that grow unboundedly in SPA scenarios with repeated mount/unmount.
514
+ (_a = this._onDestroy) === null || _a === void 0 ? void 0 : _a.call(this);
501
515
  }
502
516
  // ─── Called by OzVault ───────────────────────────────────────────────────
503
517
  /**
@@ -534,7 +548,7 @@ class OzElement {
534
548
  }
535
549
  this._frameWindow = (_b = (_a = this.iframe) === null || _a === void 0 ? void 0 : _a.contentWindow) !== null && _b !== void 0 ? _b : null;
536
550
  const mergedOptions = Object.assign(Object.assign({}, this.options), { style: mergeAppearanceWithElementStyle(this.appearanceStyle, this.options.style) });
537
- this.post(Object.assign({ type: 'OZ_INIT', elementType: this.elementType, options: sanitizeOptions(mergedOptions), frameId: this.frameId }, (this.fonts.length > 0 ? { fonts: this.fonts } : {})));
551
+ 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 } : {})));
538
552
  this.pendingMessages.forEach(m => this.send(m));
539
553
  this.pendingMessages = [];
540
554
  this.emit('ready', undefined);
@@ -648,13 +662,14 @@ function validateEmail(email) {
648
662
  // ─── Phone ───────────────────────────────────────────────────────────────────
649
663
  /**
650
664
  * Validates E.164 phone format: starts with +, 1–3 digit country code,
651
- * followed by 7–12 digits, total ≤50 characters.
665
+ * followed by 7–12 digits, max 15 digits total (E.164 spec cap = 16 chars
666
+ * including the leading +).
652
667
  *
653
668
  * Matches the output of checkout's formatPhoneForAPI() function.
654
669
  * Examples: "+15551234567", "+447911123456", "+61412345678"
655
670
  */
656
671
  function validateE164Phone(phone) {
657
- return /^\+[1-9]\d{6,49}$/.test(phone) && phone.length <= 50;
672
+ return /^\+[1-9]\d{6,14}$/.test(phone);
658
673
  }
659
674
  // ─── Field length ─────────────────────────────────────────────────────────────
660
675
  /** Returns true when the string is non-empty and ≤50 characters (cardSale schema). */
@@ -857,12 +872,12 @@ const PROTOCOL_VERSION = 1;
857
872
  *
858
873
  * @example
859
874
  * // Simplest — just pass sessionUrl, no need to call this directly
860
- * const vault = await OzVault.create({ pubKey: 'pk_live_...', sessionUrl: '/api/oz-session' });
875
+ * const vault = await OzVault.create({ pubKey: 'pk_prod_...', sessionUrl: '/api/oz-session' });
861
876
  *
862
877
  * @example
863
878
  * // Manual — use when you need custom headers
864
879
  * const vault = await OzVault.create({
865
- * pubKey: 'pk_live_...',
880
+ * pubKey: 'pk_prod_...',
866
881
  * getSessionKey: createSessionFetcher('/api/oz-session'),
867
882
  * });
868
883
  */
@@ -900,7 +915,22 @@ function createSessionFetcher(url) {
900
915
  throw new OzError(`Could not reach session endpoint (${url}): ${msg}`, undefined, 'network');
901
916
  }
902
917
  }
903
- const data = await res.json().catch(() => ({}));
918
+ // Parse JSON separately from the ok-check so that a non-JSON error body
919
+ // (HTML error page, WAF block, CDN 503) produces the right error code.
920
+ // Previously res.json() was attempted before res.ok was checked; a parse
921
+ // failure on a 5xx HTML body would fall through as {} and produce a
922
+ // misleading 'validation' code when the real cause is a server/network issue.
923
+ let data = {};
924
+ try {
925
+ data = await res.json();
926
+ }
927
+ catch (_a) {
928
+ if (!res.ok) {
929
+ 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');
930
+ }
931
+ // HTTP 200 but body isn't JSON — this is a misconfigured session endpoint.
932
+ throw new OzError('Session endpoint returned HTTP 200 but the response body is not valid JSON. Check your /api/oz-session implementation.', undefined, 'validation');
933
+ }
904
934
  if (!res.ok) {
905
935
  throw new OzError(typeof data.error === 'string' && data.error
906
936
  ? data.error
@@ -918,10 +948,19 @@ function createSessionFetcher(url) {
918
948
  }
919
949
 
920
950
  function isCardMetadata(v) {
921
- return !!v && typeof v === 'object' && typeof v.last4 === 'string';
951
+ if (!v || typeof v !== 'object')
952
+ return false;
953
+ const r = v;
954
+ return (typeof r.last4 === 'string' &&
955
+ typeof r.brand === 'string' &&
956
+ typeof r.expMonth === 'string' &&
957
+ typeof r.expYear === 'string');
922
958
  }
923
959
  function isBankAccountMetadata(v) {
924
- return !!v && typeof v === 'object' && typeof v.last4 === 'string';
960
+ if (!v || typeof v !== 'object')
961
+ return false;
962
+ const r = v;
963
+ return typeof r.last4 === 'string' && typeof r.routingNumberLast4 === 'string';
925
964
  }
926
965
  const DEFAULT_FRAME_BASE_URL = "https://elements.ozura.com";
927
966
  /**
@@ -931,16 +970,10 @@ const DEFAULT_FRAME_BASE_URL = "https://elements.ozura.com";
931
970
  * Use the static `OzVault.create()` factory — do not call `new OzVault()` directly.
932
971
  *
933
972
  * @example
973
+ * // Recommended — pass sessionUrl and let the SDK call your backend automatically
934
974
  * const vault = await OzVault.create({
935
- * pubKey: 'pk_live_...',
936
- * fetchWaxKey: async (sessionId) => {
937
- * // Call your backend — which calls ozura.mintWaxKey() from @ozura/elements/server
938
- * const { waxKey } = await fetch('/api/mint-wax', {
939
- * method: 'POST',
940
- * body: JSON.stringify({ sessionId }),
941
- * }).then(r => r.json());
942
- * return waxKey;
943
- * },
975
+ * pubKey: 'pk_prod_...', // or 'pk_test_...' for test mode
976
+ * sessionUrl: '/api/oz-session', // backend endpoint that calls ozura.createSession()
944
977
  * });
945
978
  * const cardNum = vault.createElement('cardNumber');
946
979
  * cardNum.mount('#card-number');
@@ -986,6 +1019,11 @@ class OzVault {
986
1019
  this.tokenizationSessionId = tokenizationSessionId;
987
1020
  this.pubKey = options.pubKey;
988
1021
  this.frameBaseUrl = options.frameBaseUrl || DEFAULT_FRAME_BASE_URL;
1022
+ // Validate immediately after assignment
1023
+ if (!this.frameBaseUrl.startsWith('https://') &&
1024
+ !this.frameBaseUrl.startsWith('http://localhost')) {
1025
+ throw new OzError('frameBaseUrl must use HTTPS (http://localhost is allowed for local development)', undefined, 'config');
1026
+ }
989
1027
  this.frameOrigin = new URL(this.frameBaseUrl).origin;
990
1028
  this.fonts = (_a = options.fonts) !== null && _a !== void 0 ? _a : [];
991
1029
  this.resolvedAppearance = resolveAppearance(options.appearance);
@@ -1094,7 +1132,7 @@ class OzVault {
1094
1132
  // the OZ_INIT sent at that point had an empty waxKey. Send a follow-up now
1095
1133
  // so the tokenizer has the key stored before any createToken() call.
1096
1134
  if (vault.tokenizerReady) {
1097
- vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey });
1135
+ vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey, debug: vault._debug });
1098
1136
  }
1099
1137
  vault.log('wax key received — vault ready');
1100
1138
  return vault;
@@ -1176,7 +1214,12 @@ class OzVault {
1176
1214
  this.completionState.delete(existing.frameId);
1177
1215
  existing.destroy();
1178
1216
  }
1179
- const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance);
1217
+ const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance, () => {
1218
+ // Prune vault-level maps when the element is manually destroyed so they
1219
+ // don't grow unboundedly in SPA scenarios with repeated mount/unmount cycles.
1220
+ this.elements.delete(el.frameId);
1221
+ this.completionState.delete(el.frameId);
1222
+ }, this._debug);
1180
1223
  this.elements.set(el.frameId, el);
1181
1224
  typeMap.set(type, el);
1182
1225
  return el;
@@ -1259,7 +1302,6 @@ class OzVault {
1259
1302
  this.sendToTokenizer({
1260
1303
  type: 'OZ_BANK_TOKENIZE',
1261
1304
  requestId,
1262
- waxKey: this.waxKey,
1263
1305
  tokenizationSessionId: this.tokenizationSessionId,
1264
1306
  pubKey: this.pubKey,
1265
1307
  firstName: options.firstName.trim(),
@@ -1267,7 +1309,10 @@ class OzVault {
1267
1309
  fieldCount: readyBankElements.length,
1268
1310
  }, bankChannels.map(ch => ch.port1));
1269
1311
  this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
1270
- readyBankElements.forEach((el, i) => el.beginCollect(requestId, bankChannels[i].port2));
1312
+ readyBankElements.forEach((el, i) => {
1313
+ this.log('OZ_BEGIN_COLLECT dispatched', { type: el.type, requestIdPrefix: `${requestId.slice(0, 12)}...` });
1314
+ el.beginCollect(requestId, bankChannels[i].port2);
1315
+ });
1271
1316
  const bankTimeoutId = setTimeout(() => {
1272
1317
  if (this.bankTokenizeResolvers.has(requestId)) {
1273
1318
  this.bankTokenizeResolvers.delete(requestId);
@@ -1351,7 +1396,11 @@ class OzVault {
1351
1396
  }
1352
1397
  this._tokenizing = 'card';
1353
1398
  const requestId = `req-${uuid()}`;
1354
- this.log('createToken() called');
1399
+ this.log('createToken() called', {
1400
+ requestIdPrefix: requestId.slice(0, 12),
1401
+ fields: readyElements.map(el => el.type),
1402
+ billingPresent: Boolean(options.billing),
1403
+ });
1355
1404
  return new Promise((resolve, reject) => {
1356
1405
  // Capture the reset generation so cleanup() only zeros _tokenizing when it
1357
1406
  // still belongs to this invocation — not a newer one that started after a reset.
@@ -1376,7 +1425,6 @@ class OzVault {
1376
1425
  this.sendToTokenizer({
1377
1426
  type: 'OZ_TOKENIZE',
1378
1427
  requestId,
1379
- waxKey: this.waxKey,
1380
1428
  tokenizationSessionId: this.tokenizationSessionId,
1381
1429
  pubKey: this.pubKey,
1382
1430
  firstName,
@@ -1389,7 +1437,10 @@ class OzVault {
1389
1437
  if (cardEntry)
1390
1438
  cardEntry.tokenizeStartMs = tokenizeStartMs;
1391
1439
  // Tell each ready element frame to send its raw value to the tokenizer
1392
- readyElements.forEach((el, i) => el.beginCollect(requestId, cardChannels[i].port2));
1440
+ readyElements.forEach((el, i) => {
1441
+ this.log('OZ_BEGIN_COLLECT dispatched', { type: el.type, requestIdPrefix: `${requestId.slice(0, 12)}...` });
1442
+ el.beginCollect(requestId, cardChannels[i].port2);
1443
+ });
1393
1444
  const cardTimeoutId = setTimeout(() => {
1394
1445
  if (this.tokenizeResolvers.has(requestId)) {
1395
1446
  this.tokenizeResolvers.delete(requestId);
@@ -1442,6 +1493,7 @@ class OzVault {
1442
1493
  this.tokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1443
1494
  if (timeoutId != null)
1444
1495
  clearTimeout(timeoutId);
1496
+ this.log('OZ_TOKENIZE_CANCEL sent (reset)', { requestIdPrefix: `${requestId.slice(0, 12)}...` });
1445
1497
  this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1446
1498
  reject(new OzError('Vault was reset while tokenization was in progress.'));
1447
1499
  });
@@ -1449,6 +1501,7 @@ class OzVault {
1449
1501
  this.bankTokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1450
1502
  if (timeoutId != null)
1451
1503
  clearTimeout(timeoutId);
1504
+ this.log('OZ_TOKENIZE_CANCEL sent (reset, bank)', { requestIdPrefix: `${requestId.slice(0, 12)}...` });
1452
1505
  this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1453
1506
  reject(new OzError('Vault was reset while tokenization was in progress.'));
1454
1507
  });
@@ -1623,7 +1676,7 @@ class OzVault {
1623
1676
  }
1624
1677
  }
1625
1678
  handleMessage(event) {
1626
- var _a;
1679
+ var _a, _b;
1627
1680
  if (this._destroyed)
1628
1681
  return;
1629
1682
  // Only accept messages from our frame origin (defense in depth; prevents
@@ -1638,6 +1691,15 @@ class OzVault {
1638
1691
  this.handleTokenizerMessage(msg);
1639
1692
  return;
1640
1693
  }
1694
+ // OZ_TOKEN_ERROR can arrive from element frames when the MessagePort
1695
+ // transferred in OZ_BEGIN_COLLECT was dropped by the browser (e.g. the
1696
+ // frame navigated). These carry a requestId but no frameId — route them
1697
+ // through handleTokenizerMessage so the pending promise is rejected
1698
+ // immediately rather than waiting for the 30 s collect timeout.
1699
+ if (msg.type === 'OZ_TOKEN_ERROR') {
1700
+ this.handleTokenizerMessage(msg);
1701
+ return;
1702
+ }
1641
1703
  // Route to the matching element
1642
1704
  const frameId = msg.frameId;
1643
1705
  if (frameId) {
@@ -1655,6 +1717,23 @@ class OzVault {
1655
1717
  }
1656
1718
  this.log('element iframe ready', { type: el.type, frameIdPrefix: frameId.slice(0, 8) });
1657
1719
  }
1720
+ // Relay debug/warning messages from element iframes into the parent
1721
+ // DevTools console. Element frames run in cross-origin iframes whose
1722
+ // console context is invisible to developers without a frame selector switch.
1723
+ // Errors are always surfaced (genuine failures). Warnings are only emitted
1724
+ // when debug mode is on — CSS var() and font-host warnings fire on every
1725
+ // style update and would pollute production consoles otherwise.
1726
+ if (msg.type === 'OZ_DEBUG_LOG') {
1727
+ const level = typeof msg.level === 'string' ? msg.level : 'warn';
1728
+ const message = typeof msg.message === 'string' ? msg.message : String((_b = msg.message) !== null && _b !== void 0 ? _b : '');
1729
+ if (level === 'error') {
1730
+ console.error(`[OzVault:${el.type}] ${message}`);
1731
+ }
1732
+ else if (this._debug) {
1733
+ console.warn(`[OzVault:${el.type}] ${message}`);
1734
+ }
1735
+ return;
1736
+ }
1658
1737
  // Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
1659
1738
  if (msg.type === 'OZ_CHANGE') {
1660
1739
  this.handleElementChange(msg, el);
@@ -1699,7 +1778,7 @@ class OzVault {
1699
1778
  }
1700
1779
  }
1701
1780
  handleTokenizerMessage(msg) {
1702
- var _a, _b, _c, _d;
1781
+ var _a, _b, _c, _d, _e;
1703
1782
  switch (msg.type) {
1704
1783
  case 'OZ_FRAME_READY':
1705
1784
  if (msg.__ozVersion !== PROTOCOL_VERSION) {
@@ -1717,9 +1796,10 @@ class OzVault {
1717
1796
  // Deliver the wax key via OZ_INIT so the tokenizer stores it internally.
1718
1797
  // If waxKey is still empty (fetchWaxKey hasn't resolved yet), it will be
1719
1798
  // sent again from create() once the key is available.
1720
- this.sendToTokenizer(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})));
1799
+ this.sendToTokenizer(Object.assign(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})), { debug: this._debug }));
1721
1800
  (_c = this._onReady) === null || _c === void 0 ? void 0 : _c.call(this);
1722
1801
  this.log('tokenizer iframe ready', { protocolVersion: (_d = msg.__ozVersion) !== null && _d !== void 0 ? _d : null });
1802
+ this.log('vault state', this.debugState());
1723
1803
  break;
1724
1804
  case 'OZ_TOKEN_RESULT': {
1725
1805
  if (typeof msg.requestId !== 'string' || !msg.requestId) {
@@ -1745,6 +1825,7 @@ class OzVault {
1745
1825
  pending.resolve(Object.assign(Object.assign({ token,
1746
1826
  cvcSession }, (card ? { card } : {})), (pending.billing ? { billing: pending.billing } : {})));
1747
1827
  this.log('token received', {
1828
+ requestIdPrefix: msg.requestId.slice(0, 12),
1748
1829
  elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
1749
1830
  tokenPresent: true,
1750
1831
  cvcSessionPresent: true,
@@ -1776,7 +1857,7 @@ class OzVault {
1776
1857
  if (pending.timeoutId != null)
1777
1858
  clearTimeout(pending.timeoutId);
1778
1859
  const willRefresh = this.isRefreshableAuthError(errorCode, raw) && !pending.retried && Boolean(this._storedFetchWaxKey);
1779
- this.log('token error', { errorCode, willRefresh });
1860
+ this.log('token error', { requestIdPrefix: msg.requestId.slice(0, 12), errorCode, willRefresh });
1780
1861
  // Auto-refresh: if the wax key expired or was consumed and we haven't
1781
1862
  // already retried for this request, transparently re-mint and retry.
1782
1863
  if (willRefresh) {
@@ -1794,6 +1875,17 @@ class OzVault {
1794
1875
  pending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1795
1876
  return;
1796
1877
  }
1878
+ // Verify all elements from the original call are still mounted and
1879
+ // ready. If any were destroyed (e.g. the merchant called
1880
+ // createElement() or destroy() during the async refresh), the
1881
+ // beginCollect() calls below would silently no-op — the tokenizer
1882
+ // would wait forever for field values that never arrive. Reject
1883
+ // immediately with a clear message instead of hanging 30 seconds.
1884
+ const allCardElementsStillReady = pending.readyElements.every(el => this.elements.has(el.frameId) && el.isReady);
1885
+ if (!allCardElementsStillReady) {
1886
+ pending.reject(new OzError('Card fields changed during session refresh. Please re-enter your card details.'));
1887
+ return;
1888
+ }
1797
1889
  const newRequestId = `req-${uuid()}`;
1798
1890
  // _tokenizing is still 'card' (cleanup() hasn't been called yet)
1799
1891
  this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
@@ -1802,14 +1894,16 @@ class OzVault {
1802
1894
  this.sendToTokenizer({
1803
1895
  type: 'OZ_TOKENIZE',
1804
1896
  requestId: newRequestId,
1805
- waxKey: this.waxKey,
1806
1897
  tokenizationSessionId: this.tokenizationSessionId,
1807
1898
  pubKey: this.pubKey,
1808
1899
  firstName: pending.firstName,
1809
1900
  lastName: pending.lastName,
1810
1901
  fieldCount: pending.fieldCount,
1811
1902
  }, retryCardChannels.map(ch => ch.port1));
1812
- pending.readyElements.forEach((el, i) => el.beginCollect(newRequestId, retryCardChannels[i].port2));
1903
+ pending.readyElements.forEach((el, i) => {
1904
+ this.log('OZ_BEGIN_COLLECT dispatched (retry)', { type: el.type, requestIdPrefix: `${newRequestId.slice(0, 12)}...` });
1905
+ el.beginCollect(newRequestId, retryCardChannels[i].port2);
1906
+ });
1813
1907
  const retryCardTimeoutId = setTimeout(() => {
1814
1908
  if (this.tokenizeResolvers.has(newRequestId)) {
1815
1909
  this.tokenizeResolvers.delete(newRequestId);
@@ -1826,68 +1920,80 @@ class OzVault {
1826
1920
  pending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry tokenization failed to start'));
1827
1921
  }
1828
1922
  }).catch((refreshErr) => {
1829
- const msg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
1830
- pending.reject(new OzError(msg, undefined, 'auth'));
1923
+ const errMsg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
1924
+ pending.reject(new OzError(errMsg, undefined, 'auth'));
1831
1925
  });
1832
1926
  break;
1833
1927
  }
1834
1928
  pending.reject(new OzError(normalizeVaultError(raw), raw, errorCode));
1835
1929
  }
1836
- // Also check bank resolvers — both card and bank errors use OZ_TOKEN_ERROR
1837
- const bankPending = this.bankTokenizeResolvers.get(msg.requestId);
1838
- if (bankPending) {
1839
- this.bankTokenizeResolvers.delete(msg.requestId);
1840
- if (bankPending.timeoutId != null)
1841
- clearTimeout(bankPending.timeoutId);
1842
- if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
1843
- const resetCountAtRetry = this._resetCount;
1844
- this.refreshWaxKey().then(() => {
1845
- if (this._destroyed) {
1846
- bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
1847
- return;
1848
- }
1849
- if (this._resetCount !== resetCountAtRetry) {
1850
- bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1851
- return;
1852
- }
1853
- const newRequestId = `req-${uuid()}`;
1854
- this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
1855
- try {
1856
- const retryBankChannels = bankPending.readyElements.map(() => new MessageChannel());
1857
- this.sendToTokenizer({
1858
- type: 'OZ_BANK_TOKENIZE',
1859
- requestId: newRequestId,
1860
- waxKey: this.waxKey,
1861
- tokenizationSessionId: this.tokenizationSessionId,
1862
- pubKey: this.pubKey,
1863
- firstName: bankPending.firstName,
1864
- lastName: bankPending.lastName,
1865
- fieldCount: bankPending.fieldCount,
1866
- }, retryBankChannels.map(ch => ch.port1));
1867
- bankPending.readyElements.forEach((el, i) => el.beginCollect(newRequestId, retryBankChannels[i].port2));
1868
- const retryBankTimeoutId = setTimeout(() => {
1869
- if (this.bankTokenizeResolvers.has(newRequestId)) {
1870
- this.bankTokenizeResolvers.delete(newRequestId);
1871
- this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId: newRequestId });
1872
- bankPending.reject(new OzError('Bank tokenization timed out after wax key refresh.', undefined, 'timeout'));
1873
- }
1874
- }, 30000);
1875
- const retryBankEntry = this.bankTokenizeResolvers.get(newRequestId);
1876
- if (retryBankEntry)
1877
- retryBankEntry.timeoutId = retryBankTimeoutId;
1878
- }
1879
- catch (setupErr) {
1880
- this.bankTokenizeResolvers.delete(newRequestId);
1881
- bankPending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry bank tokenization failed to start'));
1882
- }
1883
- }).catch((refreshErr) => {
1884
- const msg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
1885
- bankPending.reject(new OzError(msg, undefined, 'auth'));
1886
- });
1887
- break;
1930
+ else {
1931
+ // Also check bank resolvers — both card and bank errors use OZ_TOKEN_ERROR.
1932
+ // Use else-if rather than sequential checks so a UUID collision (however
1933
+ // improbable) can never trigger double-rejection of two unrelated resolvers.
1934
+ const bankPending = this.bankTokenizeResolvers.get(msg.requestId);
1935
+ if (bankPending) {
1936
+ this.bankTokenizeResolvers.delete(msg.requestId);
1937
+ if (bankPending.timeoutId != null)
1938
+ clearTimeout(bankPending.timeoutId);
1939
+ if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
1940
+ const resetCountAtRetry = this._resetCount;
1941
+ this.refreshWaxKey().then(() => {
1942
+ if (this._destroyed) {
1943
+ bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
1944
+ return;
1945
+ }
1946
+ if (this._resetCount !== resetCountAtRetry) {
1947
+ bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1948
+ return;
1949
+ }
1950
+ // Same stale-element guard as the card retry path above.
1951
+ const allBankElementsStillReady = bankPending.readyElements.every(el => this.elements.has(el.frameId) && el.isReady);
1952
+ if (!allBankElementsStillReady) {
1953
+ bankPending.reject(new OzError('Bank fields changed during session refresh. Please re-enter your account details.'));
1954
+ return;
1955
+ }
1956
+ const newRequestId = `req-${uuid()}`;
1957
+ this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
1958
+ try {
1959
+ const retryBankChannels = bankPending.readyElements.map(() => new MessageChannel());
1960
+ this.sendToTokenizer({
1961
+ type: 'OZ_BANK_TOKENIZE',
1962
+ requestId: newRequestId,
1963
+ tokenizationSessionId: this.tokenizationSessionId,
1964
+ pubKey: this.pubKey,
1965
+ firstName: bankPending.firstName,
1966
+ lastName: bankPending.lastName,
1967
+ fieldCount: bankPending.fieldCount,
1968
+ }, retryBankChannels.map(ch => ch.port1));
1969
+ bankPending.readyElements.forEach((el, i) => {
1970
+ this.log('OZ_BEGIN_COLLECT dispatched (retry)', { type: el.type, requestIdPrefix: `${newRequestId.slice(0, 12)}...` });
1971
+ el.beginCollect(newRequestId, retryBankChannels[i].port2);
1972
+ });
1973
+ const retryBankTimeoutId = setTimeout(() => {
1974
+ if (this.bankTokenizeResolvers.has(newRequestId)) {
1975
+ this.bankTokenizeResolvers.delete(newRequestId);
1976
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId: newRequestId });
1977
+ bankPending.reject(new OzError('Bank tokenization timed out after wax key refresh.', undefined, 'timeout'));
1978
+ }
1979
+ }, 30000);
1980
+ const retryBankEntry = this.bankTokenizeResolvers.get(newRequestId);
1981
+ if (retryBankEntry)
1982
+ retryBankEntry.timeoutId = retryBankTimeoutId;
1983
+ }
1984
+ catch (setupErr) {
1985
+ this.bankTokenizeResolvers.delete(newRequestId);
1986
+ bankPending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry bank tokenization failed to start'));
1987
+ }
1988
+ }).catch((refreshErr) => {
1989
+ const errMsg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
1990
+ bankPending.reject(new OzError(errMsg, undefined, 'auth'));
1991
+ });
1992
+ break;
1993
+ }
1994
+ bankPending.reject(new OzError(normalizeBankVaultError(raw), raw, errorCode));
1888
1995
  }
1889
- bankPending.reject(new OzError(normalizeBankVaultError(raw), raw, errorCode));
1890
- }
1996
+ } // end else (bank path)
1891
1997
  break;
1892
1998
  }
1893
1999
  case 'OZ_BANK_TOKEN_RESULT': {
@@ -1923,6 +2029,23 @@ class OzVault {
1923
2029
  }
1924
2030
  break;
1925
2031
  }
2032
+ case 'OZ_DEBUG_LOG': {
2033
+ // Relay warnings/errors from the tokenizer iframe into the parent page's
2034
+ // DevTools console. The tokenizer runs in a cross-origin iframe whose
2035
+ // console context is invisible to most developers unless they manually
2036
+ // switch the DevTools frame selector.
2037
+ // Errors are always surfaced; warnings only in debug mode to avoid
2038
+ // polluting production consoles.
2039
+ const level = typeof msg.level === 'string' ? msg.level : 'warn';
2040
+ const message = typeof msg.message === 'string' ? msg.message : String((_e = msg.message) !== null && _e !== void 0 ? _e : '');
2041
+ if (level === 'error') {
2042
+ console.error(`[OzVault:tokenizer] ${message}`);
2043
+ }
2044
+ else if (this._debug) {
2045
+ console.warn(`[OzVault:tokenizer] ${message}`);
2046
+ }
2047
+ break;
2048
+ }
1926
2049
  }
1927
2050
  }
1928
2051
  /**
@@ -1971,6 +2094,7 @@ class OzVault {
1971
2094
  }
1972
2095
  const newSessionId = uuid();
1973
2096
  (_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
2097
+ const refreshStartMs = Date.now();
1974
2098
  this.log('wax key refresh started');
1975
2099
  this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
1976
2100
  .then(newWaxKey => {
@@ -1983,12 +2107,12 @@ class OzVault {
1983
2107
  this._tokenizeSuccessCount = 0;
1984
2108
  }
1985
2109
  if (!this._destroyed && this.tokenizerReady) {
1986
- this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey });
2110
+ this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey, debug: this._debug });
1987
2111
  }
1988
- this.log('wax key refresh succeeded');
2112
+ this.log('wax key refresh succeeded', { durationMs: Date.now() - refreshStartMs });
1989
2113
  })
1990
2114
  .catch((err) => {
1991
- this.log('wax key refresh failed', { error: err instanceof Error ? err.message : String(err) });
2115
+ this.log('wax key refresh failed', { error: err instanceof Error ? err.message : String(err), durationMs: Date.now() - refreshStartMs });
1992
2116
  throw err;
1993
2117
  })
1994
2118
  .finally(() => {