@ozura/elements 1.0.2-next.16 → 1.0.2-next.18

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.
@@ -935,7 +935,7 @@
935
935
  * @internal
936
936
  */
937
937
  constructor(options, waxKey, tokenizationSessionId) {
938
- var _a, _b, _c;
938
+ var _a, _b, _c, _d;
939
939
  this.elements = new Map();
940
940
  this.elementsByType = new Map();
941
941
  this.bankElementsByType = new Map();
@@ -948,6 +948,9 @@
948
948
  this.tokenizerReady = false;
949
949
  this._tokenizing = null;
950
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;
951
954
  // Tracks successful tokenizations against the per-key call budget so the SDK
952
955
  // can proactively refresh the wax key after it has been consumed rather than
953
956
  // waiting for the next createToken() call to fail.
@@ -967,13 +970,14 @@
967
970
  this.resolvedAppearance = resolveAppearance(options.appearance);
968
971
  this.vaultId = `vault-${uuid()}`;
969
972
  this._maxTokenizeCalls = (_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3;
973
+ this._debug = (_c = options.debug) !== null && _c !== void 0 ? _c : false;
970
974
  this.boundHandleMessage = this.handleMessage.bind(this);
971
975
  window.addEventListener('message', this.boundHandleMessage);
972
976
  this.boundHandleVisibility = this.handleVisibilityChange.bind(this);
973
977
  document.addEventListener('visibilitychange', this.boundHandleVisibility);
974
978
  this.mountTokenizerFrame();
975
979
  if (options.onLoadError) {
976
- const timeout = (_c = options.loadTimeoutMs) !== null && _c !== void 0 ? _c : 10000;
980
+ const timeout = (_d = options.loadTimeoutMs) !== null && _d !== void 0 ? _d : 10000;
977
981
  this.loadErrorTimeoutId = setTimeout(() => {
978
982
  this.loadErrorTimeoutId = null;
979
983
  if (!this._destroyed && !this.tokenizerReady) {
@@ -983,6 +987,7 @@
983
987
  }
984
988
  this._onWaxRefresh = options.onWaxRefresh;
985
989
  this._onReady = options.onReady;
990
+ this.log('vault created', { vaultId: this.vaultId, frameBaseUrl: this.frameBaseUrl, maxTokenizeCalls: this._maxTokenizeCalls });
986
991
  }
987
992
  /**
988
993
  * Creates and returns a ready `OzVault` instance.
@@ -1052,6 +1057,7 @@
1052
1057
  if (vault.tokenizerReady) {
1053
1058
  vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey });
1054
1059
  }
1060
+ vault.log('wax key received — vault ready');
1055
1061
  return vault;
1056
1062
  }
1057
1063
  /**
@@ -1193,8 +1199,13 @@
1193
1199
  const readyBankElements = [accountEl, routingEl];
1194
1200
  this._tokenizing = 'bank';
1195
1201
  const requestId = `req-${uuid()}`;
1202
+ this.log('createBankToken() called');
1196
1203
  return new Promise((resolve, reject) => {
1197
- const cleanup = () => { this._tokenizing = null; };
1204
+ const resetCountAtStart = this._resetCount;
1205
+ const cleanup = () => {
1206
+ if (this._resetCount === resetCountAtStart)
1207
+ this._tokenizing = null;
1208
+ };
1198
1209
  this.bankTokenizeResolvers.set(requestId, {
1199
1210
  resolve: (v) => { cleanup(); resolve(v); },
1200
1211
  reject: (e) => { cleanup(); reject(e); },
@@ -1205,6 +1216,7 @@
1205
1216
  });
1206
1217
  try {
1207
1218
  const bankChannels = readyBankElements.map(() => new MessageChannel());
1219
+ const bankTokenizeStartMs = Date.now();
1208
1220
  this.sendToTokenizer({
1209
1221
  type: 'OZ_BANK_TOKENIZE',
1210
1222
  requestId,
@@ -1215,6 +1227,7 @@
1215
1227
  lastName: options.lastName.trim(),
1216
1228
  fieldCount: readyBankElements.length,
1217
1229
  }, bankChannels.map(ch => ch.port1));
1230
+ this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
1218
1231
  readyBankElements.forEach((el, i) => el.beginCollect(requestId, bankChannels[i].port2));
1219
1232
  const bankTimeoutId = setTimeout(() => {
1220
1233
  if (this.bankTokenizeResolvers.has(requestId)) {
@@ -1225,8 +1238,10 @@
1225
1238
  }
1226
1239
  }, 30000);
1227
1240
  const bankPendingEntry = this.bankTokenizeResolvers.get(requestId);
1228
- if (bankPendingEntry)
1241
+ if (bankPendingEntry) {
1229
1242
  bankPendingEntry.timeoutId = bankTimeoutId;
1243
+ bankPendingEntry.tokenizeStartMs = bankTokenizeStartMs;
1244
+ }
1230
1245
  }
1231
1246
  catch (err) {
1232
1247
  this.bankTokenizeResolvers.delete(requestId);
@@ -1297,8 +1312,15 @@
1297
1312
  }
1298
1313
  this._tokenizing = 'card';
1299
1314
  const requestId = `req-${uuid()}`;
1315
+ this.log('createToken() called');
1300
1316
  return new Promise((resolve, reject) => {
1301
- 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
+ };
1302
1324
  this.tokenizeResolvers.set(requestId, {
1303
1325
  resolve: (v) => { cleanup(); resolve(v); },
1304
1326
  reject: (e) => { cleanup(); reject(e); },
@@ -1311,6 +1333,7 @@
1311
1333
  try {
1312
1334
  // Tell tokenizer frame to expect N field values, then tokenize
1313
1335
  const cardChannels = readyElements.map(() => new MessageChannel());
1336
+ const tokenizeStartMs = Date.now();
1314
1337
  this.sendToTokenizer({
1315
1338
  type: 'OZ_TOKENIZE',
1316
1339
  requestId,
@@ -1321,6 +1344,11 @@
1321
1344
  lastName,
1322
1345
  fieldCount: readyElements.length,
1323
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;
1324
1352
  // Tell each ready element frame to send its raw value to the tokenizer
1325
1353
  readyElements.forEach((el, i) => el.beginCollect(requestId, cardChannels[i].port2));
1326
1354
  const cardTimeoutId = setTimeout(() => {
@@ -1367,8 +1395,11 @@
1367
1395
  reset() {
1368
1396
  if (this._destroyed)
1369
1397
  return;
1398
+ const cancelling = Boolean(this._tokenizing);
1399
+ this.log('reset() called', { tokenizing: this._tokenizing, cancelling });
1370
1400
  if (this._tokenizing) {
1371
1401
  this._tokenizing = null;
1402
+ this._resetCount++;
1372
1403
  this.tokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1373
1404
  if (timeoutId != null)
1374
1405
  clearTimeout(timeoutId);
@@ -1406,6 +1437,7 @@
1406
1437
  if (this._destroyed)
1407
1438
  return;
1408
1439
  this._destroyed = true;
1440
+ this.log('destroy() called');
1409
1441
  window.removeEventListener('message', this.boundHandleMessage);
1410
1442
  document.removeEventListener('visibilitychange', this.boundHandleVisibility);
1411
1443
  if (this._pendingMount) {
@@ -1460,13 +1492,17 @@
1460
1492
  const REFRESH_THRESHOLD_MS = 20 * 60 * 1000; // 20 minutes
1461
1493
  if (document.hidden) {
1462
1494
  this._hiddenAt = Date.now();
1495
+ this.log('tab hidden');
1463
1496
  }
1464
1497
  else {
1465
- if (this._hiddenAt !== null &&
1466
- Date.now() - this._hiddenAt >= REFRESH_THRESHOLD_MS &&
1467
- 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) &&
1468
1502
  !this._tokenizing &&
1469
- !this._waxRefreshing) {
1503
+ !this._waxRefreshing);
1504
+ this.log('tab visible', { hiddenMs, willRefresh });
1505
+ if (willRefresh) {
1470
1506
  this.refreshWaxKey().catch((err) => {
1471
1507
  // Proactive refresh failure is non-fatal — the reactive path on the
1472
1508
  // next createToken() call will handle it, including the auth retry.
@@ -1476,6 +1512,56 @@
1476
1512
  this._hiddenAt = null;
1477
1513
  }
1478
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
+ }
1479
1565
  mountTokenizerFrame() {
1480
1566
  const mount = () => {
1481
1567
  this._pendingMount = null;
@@ -1487,6 +1573,7 @@
1487
1573
  iframe.src = `${this.frameBaseUrl}/frame/tokenizer-frame.html#vaultId=${encodeURIComponent(this.vaultId)}${parentOrigin ? `&parentOrigin=${encodeURIComponent(parentOrigin)}` : ''}`;
1488
1574
  document.body.appendChild(iframe);
1489
1575
  this.tokenizerFrame = iframe;
1576
+ this.log('mounting tokenizer iframe');
1490
1577
  };
1491
1578
  if (document.readyState === 'loading') {
1492
1579
  this._pendingMount = mount;
@@ -1527,6 +1614,7 @@
1527
1614
  `SDK expects v${PROTOCOL_VERSION}, frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
1528
1615
  'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
1529
1616
  }
1617
+ this.log('element iframe ready', { type: el.type, frameIdPrefix: frameId.slice(0, 8) });
1530
1618
  }
1531
1619
  // Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
1532
1620
  if (msg.type === 'OZ_CHANGE') {
@@ -1550,6 +1638,7 @@
1550
1638
  // Require valid too — avoids advancing at 13 digits for unknown-brand cards
1551
1639
  // where isComplete() fires before the user has finished typing.
1552
1640
  const justCompleted = complete && valid && !wasComplete;
1641
+ this.log('field changed', { type: el.type, complete, valid, justCompleted });
1553
1642
  // Sync CVV length when card brand changes
1554
1643
  if (el.type === 'cardNumber') {
1555
1644
  const brand = msg.cardBrand;
@@ -1561,15 +1650,17 @@
1561
1650
  // Auto-advance focus on completion
1562
1651
  if (justCompleted) {
1563
1652
  if (el.type === 'cardNumber') {
1653
+ this.log('auto-advance', { from: 'cardNumber', to: 'expirationDate' });
1564
1654
  (_b = this.elementsByType.get('expirationDate')) === null || _b === void 0 ? void 0 : _b.focus();
1565
1655
  }
1566
1656
  else if (el.type === 'expirationDate') {
1657
+ this.log('auto-advance', { from: 'expirationDate', to: 'cvv' });
1567
1658
  (_c = this.elementsByType.get('cvv')) === null || _c === void 0 ? void 0 : _c.focus();
1568
1659
  }
1569
1660
  }
1570
1661
  }
1571
1662
  handleTokenizerMessage(msg) {
1572
- var _a, _b, _c;
1663
+ var _a, _b, _c, _d;
1573
1664
  switch (msg.type) {
1574
1665
  case 'OZ_FRAME_READY':
1575
1666
  if (msg.__ozVersion !== PROTOCOL_VERSION) {
@@ -1589,6 +1680,7 @@
1589
1680
  // sent again from create() once the key is available.
1590
1681
  this.sendToTokenizer(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})));
1591
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 });
1592
1684
  break;
1593
1685
  case 'OZ_TOKEN_RESULT': {
1594
1686
  if (typeof msg.requestId !== 'string' || !msg.requestId) {
@@ -1613,11 +1705,18 @@
1613
1705
  }
1614
1706
  pending.resolve(Object.assign(Object.assign({ token,
1615
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
+ });
1616
1714
  // Increment the per-key success counter and proactively refresh once
1617
1715
  // the budget is exhausted so the next createToken() call uses a fresh
1618
1716
  // key without waiting for a vault rejection.
1619
1717
  this._tokenizeSuccessCount++;
1620
1718
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1719
+ this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1621
1720
  this.refreshWaxKey().catch((err) => {
1622
1721
  console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1623
1722
  });
@@ -1637,14 +1736,25 @@
1637
1736
  this.tokenizeResolvers.delete(msg.requestId);
1638
1737
  if (pending.timeoutId != null)
1639
1738
  clearTimeout(pending.timeoutId);
1739
+ const willRefresh = this.isRefreshableAuthError(errorCode, raw) && !pending.retried && Boolean(this._storedFetchWaxKey);
1740
+ this.log('token error', { errorCode, willRefresh });
1640
1741
  // Auto-refresh: if the wax key expired or was consumed and we haven't
1641
1742
  // already retried for this request, transparently re-mint and retry.
1642
- if (this.isRefreshableAuthError(errorCode, raw) && !pending.retried && this._storedFetchWaxKey) {
1743
+ if (willRefresh) {
1744
+ const resetCountAtRetry = this._resetCount;
1643
1745
  this.refreshWaxKey().then(() => {
1644
1746
  if (this._destroyed) {
1645
1747
  pending.reject(new OzError('Vault destroyed during wax key refresh.'));
1646
1748
  return;
1647
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
+ }
1648
1758
  const newRequestId = `req-${uuid()}`;
1649
1759
  // _tokenizing is still 'card' (cleanup() hasn't been called yet)
1650
1760
  this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
@@ -1691,11 +1801,16 @@
1691
1801
  if (bankPending.timeoutId != null)
1692
1802
  clearTimeout(bankPending.timeoutId);
1693
1803
  if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
1804
+ const resetCountAtRetry = this._resetCount;
1694
1805
  this.refreshWaxKey().then(() => {
1695
1806
  if (this._destroyed) {
1696
1807
  bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
1697
1808
  return;
1698
1809
  }
1810
+ if (this._resetCount !== resetCountAtRetry) {
1811
+ bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1812
+ return;
1813
+ }
1699
1814
  const newRequestId = `req-${uuid()}`;
1700
1815
  this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
1701
1816
  try {
@@ -1753,9 +1868,15 @@
1753
1868
  }
1754
1869
  const bank = isBankAccountMetadata(msg.bank) ? msg.bank : undefined;
1755
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
+ });
1756
1876
  // Same proactive refresh logic as card tokenization.
1757
1877
  this._tokenizeSuccessCount++;
1758
1878
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1879
+ this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1759
1880
  this.refreshWaxKey().catch((err) => {
1760
1881
  console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1761
1882
  });
@@ -1811,6 +1932,7 @@
1811
1932
  }
1812
1933
  const newSessionId = uuid();
1813
1934
  (_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
1935
+ this.log('wax key refresh started');
1814
1936
  this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
1815
1937
  .then(newWaxKey => {
1816
1938
  if (typeof newWaxKey !== 'string' || !newWaxKey.trim()) {
@@ -1824,6 +1946,11 @@
1824
1946
  if (!this._destroyed && this.tokenizerReady) {
1825
1947
  this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey });
1826
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;
1827
1954
  })
1828
1955
  .finally(() => {
1829
1956
  this._waxRefreshing = null;