@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.
package/README.md CHANGED
@@ -28,6 +28,7 @@ Card data is collected inside Ozura-hosted iframes so raw numbers never touch yo
28
28
  - [vault.createBankToken()](#vaultcreatebanktokenoptions)
29
29
  - [vault.reset()](#vaultreset)
30
30
  - [vault.destroy()](#vaultdestroy)
31
+ - [vault.debugState()](#vaultdebugstate)
31
32
  - [OzElement events](#ozelement-events)
32
33
  - [React API](#react-api)
33
34
  - [OzElements provider](#ozelements-provider)
@@ -41,6 +42,7 @@ Card data is collected inside Ozura-hosted iframes so raw numbers never touch yo
41
42
  - [Custom fonts](#custom-fonts)
42
43
  - [Billing details](#billing-details)
43
44
  - [Error handling](#error-handling)
45
+ - [Debug mode](#debug-mode)
44
46
  - [Server utilities](#server-utilities)
45
47
  - [Ozura class](#ozura-class)
46
48
  - [Route handler factories](#route-handler-factories)
@@ -361,6 +363,7 @@ Mounts the hidden tokenizer iframe and fetches the wax key concurrently. Both ha
361
363
  | `onWaxRefresh` | `() => void` | — | Called when the SDK silently re-mints an expired wax key mid-tokenization. |
362
364
  | `onReady` | `() => void` | — | Called once when the tokenizer iframe has loaded and is ready. Use in vanilla JS to re-check submit-button readiness when the tokenizer becomes ready after all element iframes have already fired. In React, `useOzElements().ready` handles this automatically. |
363
365
  | `maxTokenizeCalls` | `number` | — | Maximum successful `createToken` calls per wax key before the key is considered consumed. Default: `3`. Must match `maxTokenizeCalls` in your server-side `mintWaxKey` call. |
366
+ | `debug` | `boolean` | — | Enables structured `[OzVault]`-prefixed `console.log` output at every lifecycle event. Safe to use in production — no sensitive data is ever logged. Default: `false`. See [Debug mode](#debug-mode) for details. |
364
367
 
365
368
  Throws `OzError` if `fetchWaxKey` rejects, returns an empty string, or returns a non-string value.
366
369
 
@@ -580,6 +583,37 @@ try {
580
583
 
581
584
  ---
582
585
 
586
+ ### vault.debugState()
587
+
588
+ ```ts
589
+ vault.debugState(): Record<string, unknown>
590
+ ```
591
+
592
+ Returns a structured snapshot of the vault's internal state. Always available regardless of whether `debug: true` is set. Useful for attaching to support tickets or dumping on error.
593
+
594
+ ```ts
595
+ console.log(vault.debugState());
596
+ // {
597
+ // vaultId: 'vault_abc12...',
598
+ // isReady: true,
599
+ // tokenizing: null,
600
+ // destroyed: false,
601
+ // waxKeyPresent: true,
602
+ // tokenizeSuccessCount: 1,
603
+ // maxTokenizeCalls: 3,
604
+ // resetCount: 0,
605
+ // elements: ['cardNumber', 'expirationDate', 'cvv'],
606
+ // bankElements: [],
607
+ // completionState: { 'a1b2c3d4': true, 'e5f6a7b8': true, '...' : false },
608
+ // pendingTokenizations: 0,
609
+ // pendingBankTokenizations: 0,
610
+ // }
611
+ ```
612
+
613
+ No sensitive data is returned: wax keys, tokens, CVC sessions, and billing fields are never included.
614
+
615
+ ---
616
+
583
617
  ### OzElement events
584
618
 
585
619
  ```ts
@@ -940,6 +974,78 @@ const display = normalizeCardSaleError(err.message); // cardSale API errors
940
974
 
941
975
  ---
942
976
 
977
+ ## Debug mode
978
+
979
+ Pass `debug: true` in `VaultOptions` (or as a prop on `<OzElements>`) to activate structured console logging at every SDK lifecycle event.
980
+
981
+ ```ts
982
+ const vault = await OzVault.create({
983
+ pubKey: 'pk_live_...',
984
+ fetchWaxKey: createFetchWaxKey('/api/mint-wax'),
985
+ debug: true, // enables [OzVault] console.log output
986
+ });
987
+ ```
988
+
989
+ ```tsx
990
+ // React
991
+ <OzElements pubKey="pk_live_..." fetchWaxKey={...} debug>
992
+ ...
993
+ </OzElements>
994
+ ```
995
+
996
+ Each log entry is a `[OzVault] <message>` prefixed `console.log` call. Events logged include:
997
+
998
+ | Event | When it fires |
999
+ |---|---|
1000
+ | `vault created` | Constructor completes |
1001
+ | `wax key received` | `fetchWaxKey` resolves |
1002
+ | `mounting tokenizer iframe` | Tokenizer iframe creation begins |
1003
+ | `tokenizer iframe ready` | Tokenizer iframe handshake complete |
1004
+ | `element iframe ready` | Each card/bank input iframe loads |
1005
+ | `field changed` | Per-field `change` event (empty/complete/valid/auto-advance state) |
1006
+ | `auto-advance` | Focus moves automatically between card fields |
1007
+ | `createToken() called` | Entry to `createToken()` |
1008
+ | `OZ_TOKENIZE sent` | Tokenize request dispatched to iframe |
1009
+ | `token received` | Token result returned (with elapsed ms) |
1010
+ | `token error` | Vault or network error during tokenize |
1011
+ | `proactive wax key refresh triggered` | Budget exhausted; refresh starting |
1012
+ | `wax key refresh started/succeeded/failed` | Refresh lifecycle |
1013
+ | `tab hidden` / `tab visible` | `visibilitychange` events |
1014
+ | `reset() called` | `vault.reset()` entry |
1015
+ | `destroy() called` | `vault.destroy()` entry |
1016
+
1017
+ **Security:** No sensitive data is ever logged. Wax keys, tokens, CVC sessions, and billing fields appear only as boolean presence flags (`waxKeyPresent: true`). Frame IDs and request IDs are truncated.
1018
+
1019
+ ### vault.debugState()
1020
+
1021
+ `vault.debugState()` is always available — regardless of whether `debug: true` was set — and returns a one-time snapshot for attaching to bug reports:
1022
+
1023
+ ```ts
1024
+ console.log(vault.debugState());
1025
+ ```
1026
+
1027
+ Sample output:
1028
+
1029
+ ```json
1030
+ {
1031
+ "vaultId": "vault_abc12...",
1032
+ "isReady": true,
1033
+ "tokenizing": null,
1034
+ "destroyed": false,
1035
+ "waxKeyPresent": true,
1036
+ "tokenizeSuccessCount": 1,
1037
+ "maxTokenizeCalls": 3,
1038
+ "resetCount": 0,
1039
+ "elements": ["cardNumber", "expirationDate", "cvv"],
1040
+ "bankElements": [],
1041
+ "completionState": { "a1b2c3d4": true, "e5f6a7b8": true, "c9d0e1f2": false },
1042
+ "pendingTokenizations": 0,
1043
+ "pendingBankTokenizations": 0
1044
+ }
1045
+ ```
1046
+
1047
+ ---
1048
+
943
1049
  ## Server utilities
944
1050
 
945
1051
  > 📖 [Server SDK guide](https://docs.ozura.com/sdks/elements/server) — `Ozura` class methods, route handler factories, `getClientIp`, error types, and rate limits.
@@ -929,7 +929,7 @@ class OzVault {
929
929
  * @internal
930
930
  */
931
931
  constructor(options, waxKey, tokenizationSessionId) {
932
- var _a, _b, _c;
932
+ var _a, _b, _c, _d;
933
933
  this.elements = new Map();
934
934
  this.elementsByType = new Map();
935
935
  this.bankElementsByType = new Map();
@@ -942,6 +942,9 @@ class OzVault {
942
942
  this.tokenizerReady = false;
943
943
  this._tokenizing = null;
944
944
  this._destroyed = false;
945
+ // Incremented every time reset() cancels an active tokenization so that
946
+ // any in-flight wax-key refresh retry can detect it was superseded.
947
+ this._resetCount = 0;
945
948
  // Tracks successful tokenizations against the per-key call budget so the SDK
946
949
  // can proactively refresh the wax key after it has been consumed rather than
947
950
  // waiting for the next createToken() call to fail.
@@ -961,13 +964,14 @@ class OzVault {
961
964
  this.resolvedAppearance = resolveAppearance(options.appearance);
962
965
  this.vaultId = `vault-${uuid()}`;
963
966
  this._maxTokenizeCalls = (_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3;
967
+ this._debug = (_c = options.debug) !== null && _c !== void 0 ? _c : false;
964
968
  this.boundHandleMessage = this.handleMessage.bind(this);
965
969
  window.addEventListener('message', this.boundHandleMessage);
966
970
  this.boundHandleVisibility = this.handleVisibilityChange.bind(this);
967
971
  document.addEventListener('visibilitychange', this.boundHandleVisibility);
968
972
  this.mountTokenizerFrame();
969
973
  if (options.onLoadError) {
970
- const timeout = (_c = options.loadTimeoutMs) !== null && _c !== void 0 ? _c : 10000;
974
+ const timeout = (_d = options.loadTimeoutMs) !== null && _d !== void 0 ? _d : 10000;
971
975
  this.loadErrorTimeoutId = setTimeout(() => {
972
976
  this.loadErrorTimeoutId = null;
973
977
  if (!this._destroyed && !this.tokenizerReady) {
@@ -977,6 +981,7 @@ class OzVault {
977
981
  }
978
982
  this._onWaxRefresh = options.onWaxRefresh;
979
983
  this._onReady = options.onReady;
984
+ this.log('vault created', { vaultId: this.vaultId, frameBaseUrl: this.frameBaseUrl, maxTokenizeCalls: this._maxTokenizeCalls });
980
985
  }
981
986
  /**
982
987
  * Creates and returns a ready `OzVault` instance.
@@ -1046,6 +1051,7 @@ class OzVault {
1046
1051
  if (vault.tokenizerReady) {
1047
1052
  vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey });
1048
1053
  }
1054
+ vault.log('wax key received — vault ready');
1049
1055
  return vault;
1050
1056
  }
1051
1057
  /**
@@ -1187,8 +1193,13 @@ class OzVault {
1187
1193
  const readyBankElements = [accountEl, routingEl];
1188
1194
  this._tokenizing = 'bank';
1189
1195
  const requestId = `req-${uuid()}`;
1196
+ this.log('createBankToken() called');
1190
1197
  return new Promise((resolve, reject) => {
1191
- const cleanup = () => { this._tokenizing = null; };
1198
+ const resetCountAtStart = this._resetCount;
1199
+ const cleanup = () => {
1200
+ if (this._resetCount === resetCountAtStart)
1201
+ this._tokenizing = null;
1202
+ };
1192
1203
  this.bankTokenizeResolvers.set(requestId, {
1193
1204
  resolve: (v) => { cleanup(); resolve(v); },
1194
1205
  reject: (e) => { cleanup(); reject(e); },
@@ -1199,6 +1210,7 @@ class OzVault {
1199
1210
  });
1200
1211
  try {
1201
1212
  const bankChannels = readyBankElements.map(() => new MessageChannel());
1213
+ const bankTokenizeStartMs = Date.now();
1202
1214
  this.sendToTokenizer({
1203
1215
  type: 'OZ_BANK_TOKENIZE',
1204
1216
  requestId,
@@ -1209,6 +1221,7 @@ class OzVault {
1209
1221
  lastName: options.lastName.trim(),
1210
1222
  fieldCount: readyBankElements.length,
1211
1223
  }, bankChannels.map(ch => ch.port1));
1224
+ this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
1212
1225
  readyBankElements.forEach((el, i) => el.beginCollect(requestId, bankChannels[i].port2));
1213
1226
  const bankTimeoutId = setTimeout(() => {
1214
1227
  if (this.bankTokenizeResolvers.has(requestId)) {
@@ -1219,8 +1232,10 @@ class OzVault {
1219
1232
  }
1220
1233
  }, 30000);
1221
1234
  const bankPendingEntry = this.bankTokenizeResolvers.get(requestId);
1222
- if (bankPendingEntry)
1235
+ if (bankPendingEntry) {
1223
1236
  bankPendingEntry.timeoutId = bankTimeoutId;
1237
+ bankPendingEntry.tokenizeStartMs = bankTokenizeStartMs;
1238
+ }
1224
1239
  }
1225
1240
  catch (err) {
1226
1241
  this.bankTokenizeResolvers.delete(requestId);
@@ -1291,8 +1306,15 @@ class OzVault {
1291
1306
  }
1292
1307
  this._tokenizing = 'card';
1293
1308
  const requestId = `req-${uuid()}`;
1309
+ this.log('createToken() called');
1294
1310
  return new Promise((resolve, reject) => {
1295
- const cleanup = () => { this._tokenizing = null; };
1311
+ // Capture the reset generation so cleanup() only zeros _tokenizing when it
1312
+ // still belongs to this invocation — not a newer one that started after a reset.
1313
+ const resetCountAtStart = this._resetCount;
1314
+ const cleanup = () => {
1315
+ if (this._resetCount === resetCountAtStart)
1316
+ this._tokenizing = null;
1317
+ };
1296
1318
  this.tokenizeResolvers.set(requestId, {
1297
1319
  resolve: (v) => { cleanup(); resolve(v); },
1298
1320
  reject: (e) => { cleanup(); reject(e); },
@@ -1305,6 +1327,7 @@ class OzVault {
1305
1327
  try {
1306
1328
  // Tell tokenizer frame to expect N field values, then tokenize
1307
1329
  const cardChannels = readyElements.map(() => new MessageChannel());
1330
+ const tokenizeStartMs = Date.now();
1308
1331
  this.sendToTokenizer({
1309
1332
  type: 'OZ_TOKENIZE',
1310
1333
  requestId,
@@ -1315,6 +1338,11 @@ class OzVault {
1315
1338
  lastName,
1316
1339
  fieldCount: readyElements.length,
1317
1340
  }, cardChannels.map(ch => ch.port1));
1341
+ this.log('OZ_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyElements.length });
1342
+ // Store start time for elapsed-ms logging on result
1343
+ const cardEntry = this.tokenizeResolvers.get(requestId);
1344
+ if (cardEntry)
1345
+ cardEntry.tokenizeStartMs = tokenizeStartMs;
1318
1346
  // Tell each ready element frame to send its raw value to the tokenizer
1319
1347
  readyElements.forEach((el, i) => el.beginCollect(requestId, cardChannels[i].port2));
1320
1348
  const cardTimeoutId = setTimeout(() => {
@@ -1361,8 +1389,11 @@ class OzVault {
1361
1389
  reset() {
1362
1390
  if (this._destroyed)
1363
1391
  return;
1392
+ const cancelling = Boolean(this._tokenizing);
1393
+ this.log('reset() called', { tokenizing: this._tokenizing, cancelling });
1364
1394
  if (this._tokenizing) {
1365
1395
  this._tokenizing = null;
1396
+ this._resetCount++;
1366
1397
  this.tokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1367
1398
  if (timeoutId != null)
1368
1399
  clearTimeout(timeoutId);
@@ -1400,6 +1431,7 @@ class OzVault {
1400
1431
  if (this._destroyed)
1401
1432
  return;
1402
1433
  this._destroyed = true;
1434
+ this.log('destroy() called');
1403
1435
  window.removeEventListener('message', this.boundHandleMessage);
1404
1436
  document.removeEventListener('visibilitychange', this.boundHandleVisibility);
1405
1437
  if (this._pendingMount) {
@@ -1454,13 +1486,17 @@ class OzVault {
1454
1486
  const REFRESH_THRESHOLD_MS = 20 * 60 * 1000; // 20 minutes
1455
1487
  if (document.hidden) {
1456
1488
  this._hiddenAt = Date.now();
1489
+ this.log('tab hidden');
1457
1490
  }
1458
1491
  else {
1459
- if (this._hiddenAt !== null &&
1460
- Date.now() - this._hiddenAt >= REFRESH_THRESHOLD_MS &&
1461
- this._storedFetchWaxKey &&
1492
+ const hiddenMs = this._hiddenAt !== null ? Date.now() - this._hiddenAt : 0;
1493
+ const willRefresh = (this._hiddenAt !== null &&
1494
+ hiddenMs >= REFRESH_THRESHOLD_MS &&
1495
+ Boolean(this._storedFetchWaxKey) &&
1462
1496
  !this._tokenizing &&
1463
- !this._waxRefreshing) {
1497
+ !this._waxRefreshing);
1498
+ this.log('tab visible', { hiddenMs, willRefresh });
1499
+ if (willRefresh) {
1464
1500
  this.refreshWaxKey().catch((err) => {
1465
1501
  // Proactive refresh failure is non-fatal — the reactive path on the
1466
1502
  // next createToken() call will handle it, including the auth retry.
@@ -1470,6 +1506,56 @@ class OzVault {
1470
1506
  this._hiddenAt = null;
1471
1507
  }
1472
1508
  }
1509
+ // ─── Debug ───────────────────────────────────────────────────────────────
1510
+ /**
1511
+ * Emits a `[OzVault]`-prefixed entry to `console.log`. No-op when `debug` is
1512
+ * not set. Never called with sensitive values — callers use presence flags only.
1513
+ */
1514
+ log(message, data) {
1515
+ if (!this._debug)
1516
+ return;
1517
+ if (data !== undefined) {
1518
+ console.log(`[OzVault] ${message}`, data);
1519
+ }
1520
+ else {
1521
+ console.log(`[OzVault] ${message}`);
1522
+ }
1523
+ }
1524
+ /**
1525
+ * Returns a plain-object snapshot of the vault's current internal state.
1526
+ * Safe to attach to bug reports — no wax keys, tokens, or billing data included.
1527
+ *
1528
+ * Available on all vault instances regardless of whether `debug` was enabled.
1529
+ *
1530
+ * @example
1531
+ * console.log(vault.debugState());
1532
+ * // {
1533
+ * // vaultId: 'vault-abc123',
1534
+ * // isReady: true,
1535
+ * // tokenizing: null,
1536
+ * // destroyed: false,
1537
+ * // waxKeyPresent: true,
1538
+ * // elements: ['cardNumber', 'expirationDate', 'cvv'],
1539
+ * // ...
1540
+ * // }
1541
+ */
1542
+ debugState() {
1543
+ return {
1544
+ vaultId: this.vaultId,
1545
+ isReady: this.tokenizerReady,
1546
+ tokenizing: this._tokenizing,
1547
+ destroyed: this._destroyed,
1548
+ waxKeyPresent: Boolean(this.waxKey),
1549
+ tokenizeSuccessCount: this._tokenizeSuccessCount,
1550
+ maxTokenizeCalls: this._maxTokenizeCalls,
1551
+ resetCount: this._resetCount,
1552
+ elements: [...this.elementsByType.keys()],
1553
+ bankElements: [...this.bankElementsByType.keys()],
1554
+ completionState: Object.fromEntries([...this.completionState.entries()].map(([id, v]) => [id.slice(0, 8), v])),
1555
+ pendingTokenizations: this.tokenizeResolvers.size,
1556
+ pendingBankTokenizations: this.bankTokenizeResolvers.size,
1557
+ };
1558
+ }
1473
1559
  mountTokenizerFrame() {
1474
1560
  const mount = () => {
1475
1561
  this._pendingMount = null;
@@ -1481,6 +1567,7 @@ class OzVault {
1481
1567
  iframe.src = `${this.frameBaseUrl}/frame/tokenizer-frame.html#vaultId=${encodeURIComponent(this.vaultId)}${parentOrigin ? `&parentOrigin=${encodeURIComponent(parentOrigin)}` : ''}`;
1482
1568
  document.body.appendChild(iframe);
1483
1569
  this.tokenizerFrame = iframe;
1570
+ this.log('mounting tokenizer iframe');
1484
1571
  };
1485
1572
  if (document.readyState === 'loading') {
1486
1573
  this._pendingMount = mount;
@@ -1521,6 +1608,7 @@ class OzVault {
1521
1608
  `SDK expects v${PROTOCOL_VERSION}, frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
1522
1609
  'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
1523
1610
  }
1611
+ this.log('element iframe ready', { type: el.type, frameIdPrefix: frameId.slice(0, 8) });
1524
1612
  }
1525
1613
  // Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
1526
1614
  if (msg.type === 'OZ_CHANGE') {
@@ -1544,6 +1632,7 @@ class OzVault {
1544
1632
  // Require valid too — avoids advancing at 13 digits for unknown-brand cards
1545
1633
  // where isComplete() fires before the user has finished typing.
1546
1634
  const justCompleted = complete && valid && !wasComplete;
1635
+ this.log('field changed', { type: el.type, complete, valid, justCompleted });
1547
1636
  // Sync CVV length when card brand changes
1548
1637
  if (el.type === 'cardNumber') {
1549
1638
  const brand = msg.cardBrand;
@@ -1555,15 +1644,17 @@ class OzVault {
1555
1644
  // Auto-advance focus on completion
1556
1645
  if (justCompleted) {
1557
1646
  if (el.type === 'cardNumber') {
1647
+ this.log('auto-advance', { from: 'cardNumber', to: 'expirationDate' });
1558
1648
  (_b = this.elementsByType.get('expirationDate')) === null || _b === void 0 ? void 0 : _b.focus();
1559
1649
  }
1560
1650
  else if (el.type === 'expirationDate') {
1651
+ this.log('auto-advance', { from: 'expirationDate', to: 'cvv' });
1561
1652
  (_c = this.elementsByType.get('cvv')) === null || _c === void 0 ? void 0 : _c.focus();
1562
1653
  }
1563
1654
  }
1564
1655
  }
1565
1656
  handleTokenizerMessage(msg) {
1566
- var _a, _b, _c;
1657
+ var _a, _b, _c, _d;
1567
1658
  switch (msg.type) {
1568
1659
  case 'OZ_FRAME_READY':
1569
1660
  if (msg.__ozVersion !== PROTOCOL_VERSION) {
@@ -1583,6 +1674,7 @@ class OzVault {
1583
1674
  // sent again from create() once the key is available.
1584
1675
  this.sendToTokenizer(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})));
1585
1676
  (_c = this._onReady) === null || _c === void 0 ? void 0 : _c.call(this);
1677
+ this.log('tokenizer iframe ready', { protocolVersion: (_d = msg.__ozVersion) !== null && _d !== void 0 ? _d : null });
1586
1678
  break;
1587
1679
  case 'OZ_TOKEN_RESULT': {
1588
1680
  if (typeof msg.requestId !== 'string' || !msg.requestId) {
@@ -1607,11 +1699,18 @@ class OzVault {
1607
1699
  }
1608
1700
  pending.resolve(Object.assign(Object.assign({ token,
1609
1701
  cvcSession }, (card ? { card } : {})), (pending.billing ? { billing: pending.billing } : {})));
1702
+ this.log('token received', {
1703
+ elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
1704
+ tokenPresent: true,
1705
+ cvcSessionPresent: true,
1706
+ cardMetadataPresent: Boolean(card),
1707
+ });
1610
1708
  // Increment the per-key success counter and proactively refresh once
1611
1709
  // the budget is exhausted so the next createToken() call uses a fresh
1612
1710
  // key without waiting for a vault rejection.
1613
1711
  this._tokenizeSuccessCount++;
1614
1712
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1713
+ this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1615
1714
  this.refreshWaxKey().catch((err) => {
1616
1715
  console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1617
1716
  });
@@ -1631,14 +1730,25 @@ class OzVault {
1631
1730
  this.tokenizeResolvers.delete(msg.requestId);
1632
1731
  if (pending.timeoutId != null)
1633
1732
  clearTimeout(pending.timeoutId);
1733
+ const willRefresh = this.isRefreshableAuthError(errorCode, raw) && !pending.retried && Boolean(this._storedFetchWaxKey);
1734
+ this.log('token error', { errorCode, willRefresh });
1634
1735
  // Auto-refresh: if the wax key expired or was consumed and we haven't
1635
1736
  // already retried for this request, transparently re-mint and retry.
1636
- if (this.isRefreshableAuthError(errorCode, raw) && !pending.retried && this._storedFetchWaxKey) {
1737
+ if (willRefresh) {
1738
+ const resetCountAtRetry = this._resetCount;
1637
1739
  this.refreshWaxKey().then(() => {
1638
1740
  if (this._destroyed) {
1639
1741
  pending.reject(new OzError('Vault destroyed during wax key refresh.'));
1640
1742
  return;
1641
1743
  }
1744
+ if (this._resetCount !== resetCountAtRetry) {
1745
+ // reset() was called while the wax key was refreshing — the fields
1746
+ // have been cleared and _tokenizing was zeroed. Reject the original
1747
+ // promise so it doesn't stay pending, and bail out without starting
1748
+ // a new retry (which would tokenize against empty fields).
1749
+ pending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1750
+ return;
1751
+ }
1642
1752
  const newRequestId = `req-${uuid()}`;
1643
1753
  // _tokenizing is still 'card' (cleanup() hasn't been called yet)
1644
1754
  this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
@@ -1685,11 +1795,16 @@ class OzVault {
1685
1795
  if (bankPending.timeoutId != null)
1686
1796
  clearTimeout(bankPending.timeoutId);
1687
1797
  if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
1798
+ const resetCountAtRetry = this._resetCount;
1688
1799
  this.refreshWaxKey().then(() => {
1689
1800
  if (this._destroyed) {
1690
1801
  bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
1691
1802
  return;
1692
1803
  }
1804
+ if (this._resetCount !== resetCountAtRetry) {
1805
+ bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1806
+ return;
1807
+ }
1693
1808
  const newRequestId = `req-${uuid()}`;
1694
1809
  this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
1695
1810
  try {
@@ -1747,9 +1862,15 @@ class OzVault {
1747
1862
  }
1748
1863
  const bank = isBankAccountMetadata(msg.bank) ? msg.bank : undefined;
1749
1864
  pending.resolve(Object.assign({ token }, (bank ? { bank } : {})));
1865
+ this.log('bank token received', {
1866
+ elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
1867
+ tokenPresent: true,
1868
+ bankMetadataPresent: Boolean(bank),
1869
+ });
1750
1870
  // Same proactive refresh logic as card tokenization.
1751
1871
  this._tokenizeSuccessCount++;
1752
1872
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1873
+ this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1753
1874
  this.refreshWaxKey().catch((err) => {
1754
1875
  console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1755
1876
  });
@@ -1805,6 +1926,7 @@ class OzVault {
1805
1926
  }
1806
1927
  const newSessionId = uuid();
1807
1928
  (_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
1929
+ this.log('wax key refresh started');
1808
1930
  this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
1809
1931
  .then(newWaxKey => {
1810
1932
  if (typeof newWaxKey !== 'string' || !newWaxKey.trim()) {
@@ -1818,6 +1940,11 @@ class OzVault {
1818
1940
  if (!this._destroyed && this.tokenizerReady) {
1819
1941
  this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey });
1820
1942
  }
1943
+ this.log('wax key refresh succeeded');
1944
+ })
1945
+ .catch((err) => {
1946
+ this.log('wax key refresh failed', { error: err instanceof Error ? err.message : String(err) });
1947
+ throw err;
1821
1948
  })
1822
1949
  .finally(() => {
1823
1950
  this._waxRefreshing = null;