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