@ozura/elements 1.2.0-next.26 → 1.2.0-next.28

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 (35) hide show
  1. package/dist/frame/element-frame.js +35 -1
  2. package/dist/frame/element-frame.js.map +1 -1
  3. package/dist/frame/tokenizer-frame.js +111 -50
  4. package/dist/frame/tokenizer-frame.js.map +1 -1
  5. package/dist/oz-elements.esm.js +101 -68
  6. package/dist/oz-elements.esm.js.map +1 -1
  7. package/dist/oz-elements.umd.js +101 -68
  8. package/dist/oz-elements.umd.js.map +1 -1
  9. package/dist/react/frame/elementFrame.d.ts +7 -0
  10. package/dist/react/frame/tokenizerFrame.d.ts +4 -1
  11. package/dist/react/index.cjs.js +101 -68
  12. package/dist/react/index.cjs.js.map +1 -1
  13. package/dist/react/index.esm.js +101 -68
  14. package/dist/react/index.esm.js.map +1 -1
  15. package/dist/react/sdk/OzElement.d.ts +3 -1
  16. package/dist/react/sdk/OzVault.d.ts +3 -9
  17. package/dist/react/server/index.d.ts +11 -0
  18. package/dist/react/types/index.d.ts +29 -10
  19. package/dist/server/frame/elementFrame.d.ts +7 -0
  20. package/dist/server/frame/tokenizerFrame.d.ts +4 -1
  21. package/dist/server/index.cjs.js +14 -11
  22. package/dist/server/index.cjs.js.map +1 -1
  23. package/dist/server/index.esm.js +14 -11
  24. package/dist/server/index.esm.js.map +1 -1
  25. package/dist/server/sdk/OzElement.d.ts +3 -1
  26. package/dist/server/sdk/OzVault.d.ts +3 -9
  27. package/dist/server/server/index.d.ts +11 -0
  28. package/dist/server/types/index.d.ts +29 -10
  29. package/dist/types/frame/elementFrame.d.ts +7 -0
  30. package/dist/types/frame/tokenizerFrame.d.ts +4 -1
  31. package/dist/types/sdk/OzElement.d.ts +3 -1
  32. package/dist/types/sdk/OzVault.d.ts +3 -9
  33. package/dist/types/server/index.d.ts +11 -0
  34. package/dist/types/types/index.d.ts +29 -10
  35. package/package.json +1 -1
@@ -324,7 +324,7 @@ function sanitizeOptions(options) {
324
324
  * it never holds raw card data — all sensitive values live in the iframe.
325
325
  */
326
326
  class OzElement {
327
- constructor(elementType, options, vaultId, frameBaseUrl, fonts = [], appearanceStyle) {
327
+ constructor(elementType, options, vaultId, frameBaseUrl, fonts = [], appearanceStyle, onDestroy) {
328
328
  this.iframe = null;
329
329
  this._frameWindow = null;
330
330
  this._ready = false;
@@ -340,6 +340,7 @@ class OzElement {
340
340
  this.fonts = fonts;
341
341
  this.appearanceStyle = appearanceStyle;
342
342
  this.frameId = `oz-${elementType}-${uuid()}`;
343
+ this._onDestroy = onDestroy;
343
344
  }
344
345
  /** The element type this proxy represents. */
345
346
  get type() {
@@ -495,9 +496,14 @@ class OzElement {
495
496
  * and prevents future use. Distinct from `unmount()` which allows re-mounting.
496
497
  */
497
498
  destroy() {
499
+ var _a;
498
500
  this.unmount();
499
501
  this.handlers.clear();
500
502
  this._destroyed = true;
503
+ // Notify OzVault so it can prune the stale frameId entry from its elements
504
+ // and completionState maps. Without this, manually calling el.destroy() leaks
505
+ // map entries that grow unboundedly in SPA scenarios with repeated mount/unmount.
506
+ (_a = this._onDestroy) === null || _a === void 0 ? void 0 : _a.call(this);
501
507
  }
502
508
  // ─── Called by OzVault ───────────────────────────────────────────────────
503
509
  /**
@@ -900,7 +906,22 @@ function createSessionFetcher(url) {
900
906
  throw new OzError(`Could not reach session endpoint (${url}): ${msg}`, undefined, 'network');
901
907
  }
902
908
  }
903
- const data = await res.json().catch(() => ({}));
909
+ // Parse JSON separately from the ok-check so that a non-JSON error body
910
+ // (HTML error page, WAF block, CDN 503) produces the right error code.
911
+ // Previously res.json() was attempted before res.ok was checked; a parse
912
+ // failure on a 5xx HTML body would fall through as {} and produce a
913
+ // misleading 'validation' code when the real cause is a server/network issue.
914
+ let data = {};
915
+ try {
916
+ data = await res.json();
917
+ }
918
+ catch (_a) {
919
+ if (!res.ok) {
920
+ 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');
921
+ }
922
+ // HTTP 200 but body isn't JSON — this is a misconfigured session endpoint.
923
+ throw new OzError('Session endpoint returned HTTP 200 but the response body is not valid JSON. Check your /api/oz-session implementation.', undefined, 'validation');
924
+ }
904
925
  if (!res.ok) {
905
926
  throw new OzError(typeof data.error === 'string' && data.error
906
927
  ? data.error
@@ -918,10 +939,19 @@ function createSessionFetcher(url) {
918
939
  }
919
940
 
920
941
  function isCardMetadata(v) {
921
- return !!v && typeof v === 'object' && typeof v.last4 === 'string';
942
+ if (!v || typeof v !== 'object')
943
+ return false;
944
+ const r = v;
945
+ return (typeof r.last4 === 'string' &&
946
+ typeof r.brand === 'string' &&
947
+ typeof r.expMonth === 'string' &&
948
+ typeof r.expYear === 'string');
922
949
  }
923
950
  function isBankAccountMetadata(v) {
924
- return !!v && typeof v === 'object' && typeof v.last4 === 'string';
951
+ if (!v || typeof v !== 'object')
952
+ return false;
953
+ const r = v;
954
+ return typeof r.last4 === 'string' && typeof r.routingNumberLast4 === 'string';
925
955
  }
926
956
  const DEFAULT_FRAME_BASE_URL = "https://lively-hill-097170c0f.4.azurestaticapps.net";
927
957
  /**
@@ -931,16 +961,10 @@ const DEFAULT_FRAME_BASE_URL = "https://lively-hill-097170c0f.4.azurestaticapps.
931
961
  * Use the static `OzVault.create()` factory — do not call `new OzVault()` directly.
932
962
  *
933
963
  * @example
964
+ * // Recommended — pass sessionUrl and let the SDK call your backend automatically
934
965
  * const vault = await OzVault.create({
935
- * pubKey: 'pk_live_...',
936
- * fetchWaxKey: async (sessionId) => {
937
- * // Call your backend — which calls ozura.mintWaxKey() from @ozura/elements/server
938
- * const { waxKey } = await fetch('/api/mint-wax', {
939
- * method: 'POST',
940
- * body: JSON.stringify({ sessionId }),
941
- * }).then(r => r.json());
942
- * return waxKey;
943
- * },
966
+ * pubKey: 'pk_prod_...', // or 'pk_test_...' for test mode
967
+ * sessionUrl: '/api/oz-session', // backend endpoint that calls ozura.createSession()
944
968
  * });
945
969
  * const cardNum = vault.createElement('cardNumber');
946
970
  * cardNum.mount('#card-number');
@@ -1176,7 +1200,12 @@ class OzVault {
1176
1200
  this.completionState.delete(existing.frameId);
1177
1201
  existing.destroy();
1178
1202
  }
1179
- const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance);
1203
+ const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance, () => {
1204
+ // Prune vault-level maps when the element is manually destroyed so they
1205
+ // don't grow unboundedly in SPA scenarios with repeated mount/unmount cycles.
1206
+ this.elements.delete(el.frameId);
1207
+ this.completionState.delete(el.frameId);
1208
+ });
1180
1209
  this.elements.set(el.frameId, el);
1181
1210
  typeMap.set(type, el);
1182
1211
  return el;
@@ -1833,61 +1862,65 @@ class OzVault {
1833
1862
  }
1834
1863
  pending.reject(new OzError(normalizeVaultError(raw), raw, errorCode));
1835
1864
  }
1836
- // Also check bank resolvers — both card and bank errors use OZ_TOKEN_ERROR
1837
- const bankPending = this.bankTokenizeResolvers.get(msg.requestId);
1838
- if (bankPending) {
1839
- this.bankTokenizeResolvers.delete(msg.requestId);
1840
- if (bankPending.timeoutId != null)
1841
- clearTimeout(bankPending.timeoutId);
1842
- if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
1843
- const resetCountAtRetry = this._resetCount;
1844
- this.refreshWaxKey().then(() => {
1845
- if (this._destroyed) {
1846
- bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
1847
- return;
1848
- }
1849
- if (this._resetCount !== resetCountAtRetry) {
1850
- bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1851
- return;
1852
- }
1853
- const newRequestId = `req-${uuid()}`;
1854
- this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
1855
- try {
1856
- const retryBankChannels = bankPending.readyElements.map(() => new MessageChannel());
1857
- this.sendToTokenizer({
1858
- type: 'OZ_BANK_TOKENIZE',
1859
- requestId: newRequestId,
1860
- waxKey: this.waxKey,
1861
- tokenizationSessionId: this.tokenizationSessionId,
1862
- pubKey: this.pubKey,
1863
- firstName: bankPending.firstName,
1864
- lastName: bankPending.lastName,
1865
- fieldCount: bankPending.fieldCount,
1866
- }, retryBankChannels.map(ch => ch.port1));
1867
- bankPending.readyElements.forEach((el, i) => el.beginCollect(newRequestId, retryBankChannels[i].port2));
1868
- const retryBankTimeoutId = setTimeout(() => {
1869
- if (this.bankTokenizeResolvers.has(newRequestId)) {
1870
- this.bankTokenizeResolvers.delete(newRequestId);
1871
- this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId: newRequestId });
1872
- bankPending.reject(new OzError('Bank tokenization timed out after wax key refresh.', undefined, 'timeout'));
1873
- }
1874
- }, 30000);
1875
- const retryBankEntry = this.bankTokenizeResolvers.get(newRequestId);
1876
- if (retryBankEntry)
1877
- retryBankEntry.timeoutId = retryBankTimeoutId;
1878
- }
1879
- catch (setupErr) {
1880
- this.bankTokenizeResolvers.delete(newRequestId);
1881
- bankPending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry bank tokenization failed to start'));
1882
- }
1883
- }).catch((refreshErr) => {
1884
- const msg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
1885
- bankPending.reject(new OzError(msg, undefined, 'auth'));
1886
- });
1887
- break;
1865
+ else {
1866
+ // Also check bank resolvers — both card and bank errors use OZ_TOKEN_ERROR.
1867
+ // Use else-if rather than sequential checks so a UUID collision (however
1868
+ // improbable) can never trigger double-rejection of two unrelated resolvers.
1869
+ const bankPending = this.bankTokenizeResolvers.get(msg.requestId);
1870
+ if (bankPending) {
1871
+ this.bankTokenizeResolvers.delete(msg.requestId);
1872
+ if (bankPending.timeoutId != null)
1873
+ clearTimeout(bankPending.timeoutId);
1874
+ if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
1875
+ const resetCountAtRetry = this._resetCount;
1876
+ this.refreshWaxKey().then(() => {
1877
+ if (this._destroyed) {
1878
+ bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
1879
+ return;
1880
+ }
1881
+ if (this._resetCount !== resetCountAtRetry) {
1882
+ bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1883
+ return;
1884
+ }
1885
+ const newRequestId = `req-${uuid()}`;
1886
+ this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
1887
+ try {
1888
+ const retryBankChannels = bankPending.readyElements.map(() => new MessageChannel());
1889
+ this.sendToTokenizer({
1890
+ type: 'OZ_BANK_TOKENIZE',
1891
+ requestId: newRequestId,
1892
+ waxKey: this.waxKey,
1893
+ tokenizationSessionId: this.tokenizationSessionId,
1894
+ pubKey: this.pubKey,
1895
+ firstName: bankPending.firstName,
1896
+ lastName: bankPending.lastName,
1897
+ fieldCount: bankPending.fieldCount,
1898
+ }, retryBankChannels.map(ch => ch.port1));
1899
+ bankPending.readyElements.forEach((el, i) => el.beginCollect(newRequestId, retryBankChannels[i].port2));
1900
+ const retryBankTimeoutId = setTimeout(() => {
1901
+ if (this.bankTokenizeResolvers.has(newRequestId)) {
1902
+ this.bankTokenizeResolvers.delete(newRequestId);
1903
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId: newRequestId });
1904
+ bankPending.reject(new OzError('Bank tokenization timed out after wax key refresh.', undefined, 'timeout'));
1905
+ }
1906
+ }, 30000);
1907
+ const retryBankEntry = this.bankTokenizeResolvers.get(newRequestId);
1908
+ if (retryBankEntry)
1909
+ retryBankEntry.timeoutId = retryBankTimeoutId;
1910
+ }
1911
+ catch (setupErr) {
1912
+ this.bankTokenizeResolvers.delete(newRequestId);
1913
+ bankPending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry bank tokenization failed to start'));
1914
+ }
1915
+ }).catch((refreshErr) => {
1916
+ const msg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
1917
+ bankPending.reject(new OzError(msg, undefined, 'auth'));
1918
+ });
1919
+ break;
1920
+ }
1921
+ bankPending.reject(new OzError(normalizeBankVaultError(raw), raw, errorCode));
1888
1922
  }
1889
- bankPending.reject(new OzError(normalizeBankVaultError(raw), raw, errorCode));
1890
- }
1923
+ } // end else (bank path)
1891
1924
  break;
1892
1925
  }
1893
1926
  case 'OZ_BANK_TOKEN_RESULT': {