@ozura/elements 1.0.2 → 1.1.0

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.
@@ -413,7 +413,7 @@
413
413
  * (useful when integrating with React refs).
414
414
  */
415
415
  mount(target) {
416
- var _a;
416
+ var _a, _b;
417
417
  if (this._destroyed)
418
418
  throw new OzError('Cannot mount a destroyed element.');
419
419
  if (this.iframe)
@@ -430,7 +430,13 @@
430
430
  iframe.setAttribute('scrolling', 'no');
431
431
  iframe.setAttribute('allowtransparency', 'true');
432
432
  iframe.style.cssText = 'border:none;width:100%;height:46px;display:block;overflow:hidden;';
433
- iframe.title = `Secure ${this.elementType} input`;
433
+ iframe.title = `Secure ${(_a = {
434
+ cardNumber: 'card number',
435
+ expirationDate: 'expiration date',
436
+ cvv: 'CVV',
437
+ accountNumber: 'account number',
438
+ routingNumber: 'routing number',
439
+ }[this.elementType]) !== null && _a !== void 0 ? _a : this.elementType} input`;
434
440
  // Note: the `sandbox` attribute is intentionally NOT set. Field values are
435
441
  // delivered to the tokenizer iframe via a MessageChannel port (transferred
436
442
  // in OZ_BEGIN_COLLECT), so no window.parent named-frame lookup is needed.
@@ -444,7 +450,7 @@
444
450
  container.appendChild(iframe);
445
451
  this.iframe = iframe;
446
452
  this._frameWindow = iframe.contentWindow;
447
- const timeout = (_a = this.options.loadTimeoutMs) !== null && _a !== void 0 ? _a : 10000;
453
+ const timeout = (_b = this.options.loadTimeoutMs) !== null && _b !== void 0 ? _b : 10000;
448
454
  this._loadTimer = setTimeout(() => {
449
455
  if (!this._ready && !this._destroyed) {
450
456
  this.emit('loaderror', { elementType: this.elementType, error: `${this.elementType} iframe failed to load within ${timeout}ms` });
@@ -877,6 +883,19 @@
877
883
  return { valid: errors.length === 0, errors, normalized };
878
884
  }
879
885
 
886
+ /**
887
+ * Shared postMessage protocol constants.
888
+ *
889
+ * PROTOCOL_VERSION must be incremented any time a breaking change is made to
890
+ * the postMessage message shape (new required fields, renamed types, removed
891
+ * fields, changed semantics). The SDK reads this value from OZ_FRAME_READY
892
+ * messages and warns when the frame and SDK are out of sync.
893
+ *
894
+ * Non-breaking additions (new optional fields, new message types that old
895
+ * frames can safely ignore) do NOT require a version bump.
896
+ */
897
+ const PROTOCOL_VERSION = 1;
898
+
880
899
  function isCardMetadata(v) {
881
900
  return !!v && typeof v === 'object' && typeof v.last4 === 'string';
882
901
  }
@@ -916,7 +935,7 @@
916
935
  * @internal
917
936
  */
918
937
  constructor(options, waxKey, tokenizationSessionId) {
919
- var _a, _b, _c;
938
+ var _a, _b, _c, _d;
920
939
  this.elements = new Map();
921
940
  this.elementsByType = new Map();
922
941
  this.bankElementsByType = new Map();
@@ -929,6 +948,9 @@
929
948
  this.tokenizerReady = false;
930
949
  this._tokenizing = null;
931
950
  this._destroyed = false;
951
+ // Incremented every time reset() cancels an active tokenization so that
952
+ // any in-flight wax-key refresh retry can detect it was superseded.
953
+ this._resetCount = 0;
932
954
  // Tracks successful tokenizations against the per-key call budget so the SDK
933
955
  // can proactively refresh the wax key after it has been consumed rather than
934
956
  // waiting for the next createToken() call to fail.
@@ -948,13 +970,14 @@
948
970
  this.resolvedAppearance = resolveAppearance(options.appearance);
949
971
  this.vaultId = `vault-${uuid()}`;
950
972
  this._maxTokenizeCalls = (_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3;
973
+ this._debug = (_c = options.debug) !== null && _c !== void 0 ? _c : false;
951
974
  this.boundHandleMessage = this.handleMessage.bind(this);
952
975
  window.addEventListener('message', this.boundHandleMessage);
953
976
  this.boundHandleVisibility = this.handleVisibilityChange.bind(this);
954
977
  document.addEventListener('visibilitychange', this.boundHandleVisibility);
955
978
  this.mountTokenizerFrame();
956
979
  if (options.onLoadError) {
957
- const timeout = (_c = options.loadTimeoutMs) !== null && _c !== void 0 ? _c : 10000;
980
+ const timeout = (_d = options.loadTimeoutMs) !== null && _d !== void 0 ? _d : 10000;
958
981
  this.loadErrorTimeoutId = setTimeout(() => {
959
982
  this.loadErrorTimeoutId = null;
960
983
  if (!this._destroyed && !this.tokenizerReady) {
@@ -964,6 +987,7 @@
964
987
  }
965
988
  this._onWaxRefresh = options.onWaxRefresh;
966
989
  this._onReady = options.onReady;
990
+ this.log('vault created', { vaultId: this.vaultId, frameBaseUrl: this.frameBaseUrl, maxTokenizeCalls: this._maxTokenizeCalls });
967
991
  }
968
992
  /**
969
993
  * Creates and returns a ready `OzVault` instance.
@@ -1033,6 +1057,7 @@
1033
1057
  if (vault.tokenizerReady) {
1034
1058
  vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey });
1035
1059
  }
1060
+ vault.log('wax key received — vault ready');
1036
1061
  return vault;
1037
1062
  }
1038
1063
  /**
@@ -1174,8 +1199,13 @@
1174
1199
  const readyBankElements = [accountEl, routingEl];
1175
1200
  this._tokenizing = 'bank';
1176
1201
  const requestId = `req-${uuid()}`;
1202
+ this.log('createBankToken() called');
1177
1203
  return new Promise((resolve, reject) => {
1178
- const cleanup = () => { this._tokenizing = null; };
1204
+ const resetCountAtStart = this._resetCount;
1205
+ const cleanup = () => {
1206
+ if (this._resetCount === resetCountAtStart)
1207
+ this._tokenizing = null;
1208
+ };
1179
1209
  this.bankTokenizeResolvers.set(requestId, {
1180
1210
  resolve: (v) => { cleanup(); resolve(v); },
1181
1211
  reject: (e) => { cleanup(); reject(e); },
@@ -1186,6 +1216,7 @@
1186
1216
  });
1187
1217
  try {
1188
1218
  const bankChannels = readyBankElements.map(() => new MessageChannel());
1219
+ const bankTokenizeStartMs = Date.now();
1189
1220
  this.sendToTokenizer({
1190
1221
  type: 'OZ_BANK_TOKENIZE',
1191
1222
  requestId,
@@ -1196,6 +1227,7 @@
1196
1227
  lastName: options.lastName.trim(),
1197
1228
  fieldCount: readyBankElements.length,
1198
1229
  }, bankChannels.map(ch => ch.port1));
1230
+ this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
1199
1231
  readyBankElements.forEach((el, i) => el.beginCollect(requestId, bankChannels[i].port2));
1200
1232
  const bankTimeoutId = setTimeout(() => {
1201
1233
  if (this.bankTokenizeResolvers.has(requestId)) {
@@ -1206,8 +1238,10 @@
1206
1238
  }
1207
1239
  }, 30000);
1208
1240
  const bankPendingEntry = this.bankTokenizeResolvers.get(requestId);
1209
- if (bankPendingEntry)
1241
+ if (bankPendingEntry) {
1210
1242
  bankPendingEntry.timeoutId = bankTimeoutId;
1243
+ bankPendingEntry.tokenizeStartMs = bankTokenizeStartMs;
1244
+ }
1211
1245
  }
1212
1246
  catch (err) {
1213
1247
  this.bankTokenizeResolvers.delete(requestId);
@@ -1278,8 +1312,15 @@
1278
1312
  }
1279
1313
  this._tokenizing = 'card';
1280
1314
  const requestId = `req-${uuid()}`;
1315
+ this.log('createToken() called');
1281
1316
  return new Promise((resolve, reject) => {
1282
- const cleanup = () => { this._tokenizing = null; };
1317
+ // Capture the reset generation so cleanup() only zeros _tokenizing when it
1318
+ // still belongs to this invocation — not a newer one that started after a reset.
1319
+ const resetCountAtStart = this._resetCount;
1320
+ const cleanup = () => {
1321
+ if (this._resetCount === resetCountAtStart)
1322
+ this._tokenizing = null;
1323
+ };
1283
1324
  this.tokenizeResolvers.set(requestId, {
1284
1325
  resolve: (v) => { cleanup(); resolve(v); },
1285
1326
  reject: (e) => { cleanup(); reject(e); },
@@ -1292,6 +1333,7 @@
1292
1333
  try {
1293
1334
  // Tell tokenizer frame to expect N field values, then tokenize
1294
1335
  const cardChannels = readyElements.map(() => new MessageChannel());
1336
+ const tokenizeStartMs = Date.now();
1295
1337
  this.sendToTokenizer({
1296
1338
  type: 'OZ_TOKENIZE',
1297
1339
  requestId,
@@ -1302,6 +1344,11 @@
1302
1344
  lastName,
1303
1345
  fieldCount: readyElements.length,
1304
1346
  }, cardChannels.map(ch => ch.port1));
1347
+ this.log('OZ_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyElements.length });
1348
+ // Store start time for elapsed-ms logging on result
1349
+ const cardEntry = this.tokenizeResolvers.get(requestId);
1350
+ if (cardEntry)
1351
+ cardEntry.tokenizeStartMs = tokenizeStartMs;
1305
1352
  // Tell each ready element frame to send its raw value to the tokenizer
1306
1353
  readyElements.forEach((el, i) => el.beginCollect(requestId, cardChannels[i].port2));
1307
1354
  const cardTimeoutId = setTimeout(() => {
@@ -1323,6 +1370,63 @@
1323
1370
  }
1324
1371
  });
1325
1372
  }
1373
+ /**
1374
+ * Clears all mounted element fields without tearing down the vault.
1375
+ *
1376
+ * Call this after a failed payment (e.g. card declined) to let the customer
1377
+ * re-enter their details. The vault instance, tokenizer iframe, wax key, and
1378
+ * tokenization budget counter are all preserved — no network calls are made.
1379
+ *
1380
+ * **Wax key session model:** by design, one wax key covers the full checkout
1381
+ * session. The default `max_tokenize_calls: 3` supports two declined attempts
1382
+ * and one final attempt on the same key. Do not call `vault.destroy()` and
1383
+ * recreate the vault between declines — that unnecessarily re-mints the key
1384
+ * and discards the remaining budget.
1385
+ *
1386
+ * @example
1387
+ * try {
1388
+ * const { token, cvcSession } = await vault.createToken({ billing });
1389
+ * await chargeCard(token, cvcSession);
1390
+ * } catch (err) {
1391
+ * vault.reset(); // clear fields; let customer re-enter
1392
+ * showError(err.message);
1393
+ * }
1394
+ */
1395
+ reset() {
1396
+ if (this._destroyed)
1397
+ return;
1398
+ const cancelling = Boolean(this._tokenizing);
1399
+ this.log('reset() called', { tokenizing: this._tokenizing, cancelling });
1400
+ if (this._tokenizing) {
1401
+ this._tokenizing = null;
1402
+ this._resetCount++;
1403
+ this.tokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1404
+ if (timeoutId != null)
1405
+ clearTimeout(timeoutId);
1406
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1407
+ reject(new OzError('Vault was reset while tokenization was in progress.'));
1408
+ });
1409
+ this.tokenizeResolvers.clear();
1410
+ this.bankTokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1411
+ if (timeoutId != null)
1412
+ clearTimeout(timeoutId);
1413
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1414
+ reject(new OzError('Vault was reset while tokenization was in progress.'));
1415
+ });
1416
+ this.bankTokenizeResolvers.clear();
1417
+ }
1418
+ // Clear field values in all mounted element iframes
1419
+ this.elementsByType.forEach(el => el.clear());
1420
+ this.bankElementsByType.forEach(el => el.clear());
1421
+ // Reset per-element completion state so auto-advance starts fresh on re-entry
1422
+ for (const frameId of this.completionState.keys()) {
1423
+ this.completionState.set(frameId, false);
1424
+ }
1425
+ // NOTE: _tokenizeSuccessCount is intentionally NOT reset.
1426
+ // It reflects real server-side wax key budget consumption. Zeroing it
1427
+ // would desync the proactive refresh logic from the vault's state and
1428
+ // risk triggering a mid-session re-mint on what should be a clean retry.
1429
+ }
1326
1430
  /**
1327
1431
  * Tears down the vault: removes all element iframes, the tokenizer iframe,
1328
1432
  * and the global message listener. Call this when the checkout component
@@ -1333,6 +1437,7 @@
1333
1437
  if (this._destroyed)
1334
1438
  return;
1335
1439
  this._destroyed = true;
1440
+ this.log('destroy() called');
1336
1441
  window.removeEventListener('message', this.boundHandleMessage);
1337
1442
  document.removeEventListener('visibilitychange', this.boundHandleVisibility);
1338
1443
  if (this._pendingMount) {
@@ -1387,13 +1492,17 @@
1387
1492
  const REFRESH_THRESHOLD_MS = 20 * 60 * 1000; // 20 minutes
1388
1493
  if (document.hidden) {
1389
1494
  this._hiddenAt = Date.now();
1495
+ this.log('tab hidden');
1390
1496
  }
1391
1497
  else {
1392
- if (this._hiddenAt !== null &&
1393
- Date.now() - this._hiddenAt >= REFRESH_THRESHOLD_MS &&
1394
- this._storedFetchWaxKey &&
1498
+ const hiddenMs = this._hiddenAt !== null ? Date.now() - this._hiddenAt : 0;
1499
+ const willRefresh = (this._hiddenAt !== null &&
1500
+ hiddenMs >= REFRESH_THRESHOLD_MS &&
1501
+ Boolean(this._storedFetchWaxKey) &&
1395
1502
  !this._tokenizing &&
1396
- !this._waxRefreshing) {
1503
+ !this._waxRefreshing);
1504
+ this.log('tab visible', { hiddenMs, willRefresh });
1505
+ if (willRefresh) {
1397
1506
  this.refreshWaxKey().catch((err) => {
1398
1507
  // Proactive refresh failure is non-fatal — the reactive path on the
1399
1508
  // next createToken() call will handle it, including the auth retry.
@@ -1403,6 +1512,56 @@
1403
1512
  this._hiddenAt = null;
1404
1513
  }
1405
1514
  }
1515
+ // ─── Debug ───────────────────────────────────────────────────────────────
1516
+ /**
1517
+ * Emits a `[OzVault]`-prefixed entry to `console.log`. No-op when `debug` is
1518
+ * not set. Never called with sensitive values — callers use presence flags only.
1519
+ */
1520
+ log(message, data) {
1521
+ if (!this._debug)
1522
+ return;
1523
+ if (data !== undefined) {
1524
+ console.log(`[OzVault] ${message}`, data);
1525
+ }
1526
+ else {
1527
+ console.log(`[OzVault] ${message}`);
1528
+ }
1529
+ }
1530
+ /**
1531
+ * Returns a plain-object snapshot of the vault's current internal state.
1532
+ * Safe to attach to bug reports — no wax keys, tokens, or billing data included.
1533
+ *
1534
+ * Available on all vault instances regardless of whether `debug` was enabled.
1535
+ *
1536
+ * @example
1537
+ * console.log(vault.debugState());
1538
+ * // {
1539
+ * // vaultId: 'vault-abc123',
1540
+ * // isReady: true,
1541
+ * // tokenizing: null,
1542
+ * // destroyed: false,
1543
+ * // waxKeyPresent: true,
1544
+ * // elements: ['cardNumber', 'expirationDate', 'cvv'],
1545
+ * // ...
1546
+ * // }
1547
+ */
1548
+ debugState() {
1549
+ return {
1550
+ vaultId: this.vaultId,
1551
+ isReady: this.tokenizerReady,
1552
+ tokenizing: this._tokenizing,
1553
+ destroyed: this._destroyed,
1554
+ waxKeyPresent: Boolean(this.waxKey),
1555
+ tokenizeSuccessCount: this._tokenizeSuccessCount,
1556
+ maxTokenizeCalls: this._maxTokenizeCalls,
1557
+ resetCount: this._resetCount,
1558
+ elements: [...this.elementsByType.keys()],
1559
+ bankElements: [...this.bankElementsByType.keys()],
1560
+ completionState: Object.fromEntries([...this.completionState.entries()].map(([id, v]) => [id.slice(0, 8), v])),
1561
+ pendingTokenizations: this.tokenizeResolvers.size,
1562
+ pendingBankTokenizations: this.bankTokenizeResolvers.size,
1563
+ };
1564
+ }
1406
1565
  mountTokenizerFrame() {
1407
1566
  const mount = () => {
1408
1567
  this._pendingMount = null;
@@ -1414,6 +1573,7 @@
1414
1573
  iframe.src = `${this.frameBaseUrl}/frame/tokenizer-frame.html#vaultId=${encodeURIComponent(this.vaultId)}${parentOrigin ? `&parentOrigin=${encodeURIComponent(parentOrigin)}` : ''}`;
1415
1574
  document.body.appendChild(iframe);
1416
1575
  this.tokenizerFrame = iframe;
1576
+ this.log('mounting tokenizer iframe');
1417
1577
  };
1418
1578
  if (document.readyState === 'loading') {
1419
1579
  this._pendingMount = mount;
@@ -1449,6 +1609,12 @@
1449
1609
  // the previous session and justCompleted never fires, breaking auto-advance.
1450
1610
  if (msg.type === 'OZ_FRAME_READY') {
1451
1611
  this.completionState.set(frameId, false);
1612
+ if (msg.__ozVersion !== PROTOCOL_VERSION) {
1613
+ console.warn(`[OzVault] Protocol version mismatch on element frame "${frameId}" — ` +
1614
+ `SDK expects v${PROTOCOL_VERSION}, frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
1615
+ 'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
1616
+ }
1617
+ this.log('element iframe ready', { type: el.type, frameIdPrefix: frameId.slice(0, 8) });
1452
1618
  }
1453
1619
  // Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
1454
1620
  if (msg.type === 'OZ_CHANGE') {
@@ -1472,6 +1638,7 @@
1472
1638
  // Require valid too — avoids advancing at 13 digits for unknown-brand cards
1473
1639
  // where isComplete() fires before the user has finished typing.
1474
1640
  const justCompleted = complete && valid && !wasComplete;
1641
+ this.log('field changed', { type: el.type, complete, valid, justCompleted });
1475
1642
  // Sync CVV length when card brand changes
1476
1643
  if (el.type === 'cardNumber') {
1477
1644
  const brand = msg.cardBrand;
@@ -1483,17 +1650,25 @@
1483
1650
  // Auto-advance focus on completion
1484
1651
  if (justCompleted) {
1485
1652
  if (el.type === 'cardNumber') {
1653
+ this.log('auto-advance', { from: 'cardNumber', to: 'expirationDate' });
1486
1654
  (_b = this.elementsByType.get('expirationDate')) === null || _b === void 0 ? void 0 : _b.focus();
1487
1655
  }
1488
1656
  else if (el.type === 'expirationDate') {
1657
+ this.log('auto-advance', { from: 'expirationDate', to: 'cvv' });
1489
1658
  (_c = this.elementsByType.get('cvv')) === null || _c === void 0 ? void 0 : _c.focus();
1490
1659
  }
1491
1660
  }
1492
1661
  }
1493
1662
  handleTokenizerMessage(msg) {
1494
- var _a, _b, _c;
1663
+ var _a, _b, _c, _d;
1495
1664
  switch (msg.type) {
1496
1665
  case 'OZ_FRAME_READY':
1666
+ if (msg.__ozVersion !== PROTOCOL_VERSION) {
1667
+ console.warn(`[OzVault] Protocol version mismatch — SDK expects v${PROTOCOL_VERSION}, ` +
1668
+ `tokenizer frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
1669
+ 'This usually means the deployed frame files are stale. ' +
1670
+ 'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
1671
+ }
1497
1672
  this.tokenizerReady = true;
1498
1673
  if (this.loadErrorTimeoutId != null) {
1499
1674
  clearTimeout(this.loadErrorTimeoutId);
@@ -1505,6 +1680,7 @@
1505
1680
  // sent again from create() once the key is available.
1506
1681
  this.sendToTokenizer(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})));
1507
1682
  (_c = this._onReady) === null || _c === void 0 ? void 0 : _c.call(this);
1683
+ this.log('tokenizer iframe ready', { protocolVersion: (_d = msg.__ozVersion) !== null && _d !== void 0 ? _d : null });
1508
1684
  break;
1509
1685
  case 'OZ_TOKEN_RESULT': {
1510
1686
  if (typeof msg.requestId !== 'string' || !msg.requestId) {
@@ -1529,11 +1705,18 @@
1529
1705
  }
1530
1706
  pending.resolve(Object.assign(Object.assign({ token,
1531
1707
  cvcSession }, (card ? { card } : {})), (pending.billing ? { billing: pending.billing } : {})));
1708
+ this.log('token received', {
1709
+ elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
1710
+ tokenPresent: true,
1711
+ cvcSessionPresent: true,
1712
+ cardMetadataPresent: Boolean(card),
1713
+ });
1532
1714
  // Increment the per-key success counter and proactively refresh once
1533
1715
  // the budget is exhausted so the next createToken() call uses a fresh
1534
1716
  // key without waiting for a vault rejection.
1535
1717
  this._tokenizeSuccessCount++;
1536
1718
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1719
+ this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1537
1720
  this.refreshWaxKey().catch((err) => {
1538
1721
  console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1539
1722
  });
@@ -1553,14 +1736,25 @@
1553
1736
  this.tokenizeResolvers.delete(msg.requestId);
1554
1737
  if (pending.timeoutId != null)
1555
1738
  clearTimeout(pending.timeoutId);
1739
+ const willRefresh = this.isRefreshableAuthError(errorCode, raw) && !pending.retried && Boolean(this._storedFetchWaxKey);
1740
+ this.log('token error', { errorCode, willRefresh });
1556
1741
  // Auto-refresh: if the wax key expired or was consumed and we haven't
1557
1742
  // already retried for this request, transparently re-mint and retry.
1558
- if (this.isRefreshableAuthError(errorCode, raw) && !pending.retried && this._storedFetchWaxKey) {
1743
+ if (willRefresh) {
1744
+ const resetCountAtRetry = this._resetCount;
1559
1745
  this.refreshWaxKey().then(() => {
1560
1746
  if (this._destroyed) {
1561
1747
  pending.reject(new OzError('Vault destroyed during wax key refresh.'));
1562
1748
  return;
1563
1749
  }
1750
+ if (this._resetCount !== resetCountAtRetry) {
1751
+ // reset() was called while the wax key was refreshing — the fields
1752
+ // have been cleared and _tokenizing was zeroed. Reject the original
1753
+ // promise so it doesn't stay pending, and bail out without starting
1754
+ // a new retry (which would tokenize against empty fields).
1755
+ pending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1756
+ return;
1757
+ }
1564
1758
  const newRequestId = `req-${uuid()}`;
1565
1759
  // _tokenizing is still 'card' (cleanup() hasn't been called yet)
1566
1760
  this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
@@ -1607,11 +1801,16 @@
1607
1801
  if (bankPending.timeoutId != null)
1608
1802
  clearTimeout(bankPending.timeoutId);
1609
1803
  if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
1804
+ const resetCountAtRetry = this._resetCount;
1610
1805
  this.refreshWaxKey().then(() => {
1611
1806
  if (this._destroyed) {
1612
1807
  bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
1613
1808
  return;
1614
1809
  }
1810
+ if (this._resetCount !== resetCountAtRetry) {
1811
+ bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1812
+ return;
1813
+ }
1615
1814
  const newRequestId = `req-${uuid()}`;
1616
1815
  this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
1617
1816
  try {
@@ -1669,9 +1868,15 @@
1669
1868
  }
1670
1869
  const bank = isBankAccountMetadata(msg.bank) ? msg.bank : undefined;
1671
1870
  pending.resolve(Object.assign({ token }, (bank ? { bank } : {})));
1871
+ this.log('bank token received', {
1872
+ elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
1873
+ tokenPresent: true,
1874
+ bankMetadataPresent: Boolean(bank),
1875
+ });
1672
1876
  // Same proactive refresh logic as card tokenization.
1673
1877
  this._tokenizeSuccessCount++;
1674
1878
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1879
+ this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1675
1880
  this.refreshWaxKey().catch((err) => {
1676
1881
  console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1677
1882
  });
@@ -1727,6 +1932,7 @@
1727
1932
  }
1728
1933
  const newSessionId = uuid();
1729
1934
  (_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
1935
+ this.log('wax key refresh started');
1730
1936
  this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
1731
1937
  .then(newWaxKey => {
1732
1938
  if (typeof newWaxKey !== 'string' || !newWaxKey.trim()) {
@@ -1740,6 +1946,11 @@
1740
1946
  if (!this._destroyed && this.tokenizerReady) {
1741
1947
  this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey });
1742
1948
  }
1949
+ this.log('wax key refresh succeeded');
1950
+ })
1951
+ .catch((err) => {
1952
+ this.log('wax key refresh failed', { error: err instanceof Error ? err.message : String(err) });
1953
+ throw err;
1743
1954
  })
1744
1955
  .finally(() => {
1745
1956
  this._waxRefreshing = null;