@ozura/elements 1.1.0-next.21 → 1.1.0-next.23

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.
@@ -840,6 +840,85 @@ function validateBilling(billing) {
840
840
  */
841
841
  const PROTOCOL_VERSION = 1;
842
842
 
843
+ /**
844
+ * Creates a `getSessionKey` callback for `OzVault.create()` and `<OzElements>`.
845
+ *
846
+ * This is the recommended way to wire the SDK to your backend session endpoint.
847
+ * If you don't need custom headers or auth logic, pass `sessionUrl` directly to
848
+ * `OzVault.create()` or `<OzElements>` — it calls this helper internally.
849
+ *
850
+ * The callback POSTs `{ sessionId }` to `url` and reads `sessionKey` (or the
851
+ * legacy `waxKey`) from the JSON response, so it is compatible with both the
852
+ * new `createSessionMiddleware` and the old `createMintWaxMiddleware` backends.
853
+ *
854
+ * Each call enforces a **10-second timeout**. On pure network failures
855
+ * (offline, DNS, connection refused) the request is retried **once after 750ms**.
856
+ * HTTP 4xx/5xx errors are never retried — they indicate misconfiguration.
857
+ *
858
+ * @param url - Absolute or relative URL of your session endpoint, e.g. `'/api/oz-session'`.
859
+ *
860
+ * @example
861
+ * // Simplest — just pass sessionUrl, no need to call this directly
862
+ * const vault = await OzVault.create({ pubKey: 'pk_live_...', sessionUrl: '/api/oz-session' });
863
+ *
864
+ * @example
865
+ * // Manual — use when you need custom headers
866
+ * const vault = await OzVault.create({
867
+ * pubKey: 'pk_live_...',
868
+ * getSessionKey: createSessionFetcher('/api/oz-session'),
869
+ * });
870
+ */
871
+ function createSessionFetcher(url) {
872
+ const TIMEOUT_MS = 10000;
873
+ // Each attempt gets its own AbortController so a timeout on attempt 1 does
874
+ // not bleed into the retry.
875
+ const attemptFetch = (sessionId) => {
876
+ const controller = new AbortController();
877
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
878
+ return fetch(url, {
879
+ method: 'POST',
880
+ headers: { 'Content-Type': 'application/json' },
881
+ body: JSON.stringify({ sessionId }),
882
+ signal: controller.signal,
883
+ }).finally(() => clearTimeout(timer));
884
+ };
885
+ return async (sessionId) => {
886
+ let res;
887
+ try {
888
+ res = await attemptFetch(sessionId);
889
+ }
890
+ catch (firstErr) {
891
+ // Timeout/abort — don't retry, we already waited the full duration.
892
+ if (firstErr instanceof Error && (firstErr.name === 'AbortError' || firstErr.name === 'TimeoutError')) {
893
+ throw new OzError(`Session endpoint timed out after ${TIMEOUT_MS / 1000}s (${url})`, undefined, 'timeout');
894
+ }
895
+ // Pure network error — retry once after a short pause.
896
+ await new Promise(resolve => setTimeout(resolve, 750));
897
+ try {
898
+ res = await attemptFetch(sessionId);
899
+ }
900
+ catch (retryErr) {
901
+ const msg = retryErr instanceof Error ? retryErr.message : 'Network error';
902
+ throw new OzError(`Could not reach session endpoint (${url}): ${msg}`, undefined, 'network');
903
+ }
904
+ }
905
+ const data = await res.json().catch(() => ({}));
906
+ if (!res.ok) {
907
+ throw new OzError(typeof data.error === 'string' && data.error
908
+ ? data.error
909
+ : `Session endpoint returned HTTP ${res.status}`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
910
+ }
911
+ // Accept both new `sessionKey` and legacy `waxKey` for backward compatibility
912
+ // with backends that haven't migrated to createSessionMiddleware yet.
913
+ const key = (typeof data.sessionKey === 'string' ? data.sessionKey : '') ||
914
+ (typeof data.waxKey === 'string' ? data.waxKey : '');
915
+ if (!key.trim()) {
916
+ throw new OzError('Session endpoint response is missing sessionKey. Check your /api/oz-session implementation.', undefined, 'validation');
917
+ }
918
+ return key;
919
+ };
920
+ }
921
+
843
922
  function isCardMetadata(v) {
844
923
  return !!v && typeof v === 'object' && typeof v.last4 === 'string';
845
924
  }
@@ -879,7 +958,7 @@ class OzVault {
879
958
  * @internal
880
959
  */
881
960
  constructor(options, waxKey, tokenizationSessionId) {
882
- var _a, _b, _c, _d;
961
+ var _a, _b, _c, _d, _e, _f;
883
962
  this.elements = new Map();
884
963
  this.elementsByType = new Map();
885
964
  this.bankElementsByType = new Map();
@@ -913,15 +992,16 @@ class OzVault {
913
992
  this.fonts = (_a = options.fonts) !== null && _a !== void 0 ? _a : [];
914
993
  this.resolvedAppearance = resolveAppearance(options.appearance);
915
994
  this.vaultId = `vault-${uuid()}`;
916
- this._maxTokenizeCalls = (_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3;
917
- this._debug = (_c = options.debug) !== null && _c !== void 0 ? _c : false;
995
+ // sessionLimit takes precedence over legacy maxTokenizeCalls
996
+ this._maxTokenizeCalls = (_c = (_b = options.sessionLimit) !== null && _b !== void 0 ? _b : options.maxTokenizeCalls) !== null && _c !== void 0 ? _c : 3;
997
+ this._debug = (_d = options.debug) !== null && _d !== void 0 ? _d : false;
918
998
  this.boundHandleMessage = this.handleMessage.bind(this);
919
999
  window.addEventListener('message', this.boundHandleMessage);
920
1000
  this.boundHandleVisibility = this.handleVisibilityChange.bind(this);
921
1001
  document.addEventListener('visibilitychange', this.boundHandleVisibility);
922
1002
  this.mountTokenizerFrame();
923
1003
  if (options.onLoadError) {
924
- const timeout = (_d = options.loadTimeoutMs) !== null && _d !== void 0 ? _d : 10000;
1004
+ const timeout = (_e = options.loadTimeoutMs) !== null && _e !== void 0 ? _e : 10000;
925
1005
  this.loadErrorTimeoutId = setTimeout(() => {
926
1006
  this.loadErrorTimeoutId = null;
927
1007
  if (!this._destroyed && !this.tokenizerReady) {
@@ -929,7 +1009,8 @@ class OzVault {
929
1009
  }
930
1010
  }, timeout);
931
1011
  }
932
- this._onWaxRefresh = options.onWaxRefresh;
1012
+ // onSessionRefresh takes precedence over legacy onWaxRefresh
1013
+ this._onWaxRefresh = (_f = options.onSessionRefresh) !== null && _f !== void 0 ? _f : options.onWaxRefresh;
933
1014
  this._onReady = options.onReady;
934
1015
  this.log('vault created', { vaultId: this.vaultId, frameBaseUrl: this.frameBaseUrl, maxTokenizeCalls: this._maxTokenizeCalls });
935
1016
  }
@@ -937,47 +1018,59 @@ class OzVault {
937
1018
  * Creates and returns a ready `OzVault` instance.
938
1019
  *
939
1020
  * Internally this:
940
- * 1. Generates a `tokenizationSessionId` (UUID).
1021
+ * 1. Generates a session UUID.
941
1022
  * 2. Starts loading the hidden tokenizer iframe immediately.
942
- * 3. Calls `options.fetchWaxKey(tokenizationSessionId)` concurrently — your
943
- * backend mints a session-bound wax key from the vault and returns it.
944
- * 4. Resolves with the vault instance once the wax key is stored. The iframe
1023
+ * 3. Fetches a session key from your backend concurrently — either via
1024
+ * `sessionUrl` (simplest), `getSessionKey` (custom headers/auth), or the
1025
+ * deprecated `fetchWaxKey` callback.
1026
+ * 4. Resolves with the vault instance once the session key is stored. The iframe
945
1027
  * has been loading the whole time, so `isReady` may already be true or
946
1028
  * will fire shortly after.
947
1029
  *
948
1030
  * The returned vault is ready to create elements immediately. `createToken()`
949
1031
  * additionally requires `vault.isReady` (tokenizer iframe loaded).
950
1032
  *
951
- * @throws {OzError} if `fetchWaxKey` throws, returns a non-string value, or returns an empty/whitespace-only string.
1033
+ * @throws {OzError} if the session fetch fails, times out, or returns an empty string.
952
1034
  */
953
1035
  static async create(options, signal) {
954
1036
  if (!options.pubKey || !options.pubKey.trim()) {
955
1037
  throw new OzError('pubKey is required in options. Obtain your public key from the Ozura admin.');
956
1038
  }
957
- if (typeof options.fetchWaxKey !== 'function') {
958
- throw new OzError('fetchWaxKey must be a function. See OzVault.create() docs for the expected signature.');
1039
+ // Normalize the session callback. Priority: sessionUrl > getSessionKey > fetchWaxKey (deprecated).
1040
+ // This allows merchants to use the clean new API without touching legacy code.
1041
+ let resolvedFetchKey;
1042
+ if (options.sessionUrl) {
1043
+ resolvedFetchKey = createSessionFetcher(options.sessionUrl);
1044
+ }
1045
+ else if (typeof options.getSessionKey === 'function') {
1046
+ resolvedFetchKey = options.getSessionKey;
1047
+ }
1048
+ else if (typeof options.fetchWaxKey === 'function') {
1049
+ resolvedFetchKey = options.fetchWaxKey;
1050
+ }
1051
+ else {
1052
+ throw new OzError('A session URL or callback is required. Pass sessionUrl, getSessionKey, or fetchWaxKey to OzVault.create().');
959
1053
  }
960
1054
  const tokenizationSessionId = uuid();
961
1055
  // Construct the vault immediately — this mounts the tokenizer iframe so it
962
- // starts loading while fetchWaxKey is in flight. The waxKey field starts
963
- // empty and is set below before create() returns.
1056
+ // starts loading while the session fetch is in flight.
964
1057
  const vault = new OzVault(options, '', tokenizationSessionId);
965
1058
  // If the caller provides an AbortSignal (e.g. React useEffect cleanup),
966
1059
  // destroy the vault immediately on abort so the tokenizer iframe and message
967
- // listener are removed synchronously rather than waiting for fetchWaxKey to
968
- // settle. This eliminates the brief double-iframe window in React StrictMode.
1060
+ // listener are removed synchronously rather than waiting for the session fetch
1061
+ // to settle. This eliminates the brief double-iframe window in React StrictMode.
969
1062
  const onAbort = () => vault.destroy();
970
1063
  signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onAbort, { once: true });
971
1064
  let waxKey;
972
1065
  try {
973
- waxKey = await options.fetchWaxKey(tokenizationSessionId);
1066
+ waxKey = await resolvedFetchKey(tokenizationSessionId);
974
1067
  }
975
1068
  catch (err) {
976
1069
  signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
977
1070
  vault.destroy();
978
1071
  if (signal === null || signal === void 0 ? void 0 : signal.aborted)
979
1072
  throw new OzError('OzVault.create() was cancelled.');
980
- // Preserve errorCode/retryable from OzError (e.g. timeout/network from createFetchWaxKey)
1073
+ // Preserve errorCode/retryable from OzError (e.g. timeout/network from createSessionFetcher)
981
1074
  // so callers can distinguish transient failures from config errors.
982
1075
  const originalCode = err instanceof OzError ? err.errorCode : undefined;
983
1076
  const msg = err instanceof Error ? err.message : 'Unknown error';
@@ -994,7 +1087,7 @@ class OzVault {
994
1087
  }
995
1088
  // Static methods can access private fields of instances of the same class.
996
1089
  vault.waxKey = waxKey;
997
- vault._storedFetchWaxKey = options.fetchWaxKey;
1090
+ vault._storedFetchWaxKey = resolvedFetchKey;
998
1091
  // If the tokenizer iframe fired OZ_FRAME_READY before fetchWaxKey resolved,
999
1092
  // the OZ_INIT sent at that point had an empty waxKey. Send a follow-up now
1000
1093
  // so the tokenizer has the key stored before any createToken() call.
@@ -1908,84 +2001,6 @@ class OzVault {
1908
2001
  }
1909
2002
  }
1910
2003
 
1911
- /**
1912
- * Creates a ready-to-use `fetchWaxKey` callback for `OzVault.create()` and `<OzElements>`.
1913
- *
1914
- * Calls your backend mint endpoint with `{ sessionId }` and returns the wax key string.
1915
- * Throws on non-OK responses or a missing `waxKey` field so the vault can surface the
1916
- * error through its normal error path.
1917
- *
1918
- * Each call enforces a 10-second per-attempt timeout. On a pure network-level
1919
- * failure (connection refused, DNS failure, etc.) the call is retried once after
1920
- * 750ms before throwing. HTTP errors (4xx/5xx) are never retried — they indicate
1921
- * an endpoint misconfiguration or an invalid key, not a transient failure.
1922
- *
1923
- * The mint endpoint is typically the one-line `createMintWaxHandler` / `createMintWaxMiddleware`
1924
- * from `@ozura/elements/server`.
1925
- *
1926
- * @param mintUrl - Absolute or relative URL of your wax-key mint endpoint, e.g. `'/api/mint-wax'`.
1927
- *
1928
- * @example
1929
- * // Vanilla JS
1930
- * const vault = await OzVault.create({
1931
- * pubKey: 'pk_live_...',
1932
- * fetchWaxKey: createFetchWaxKey('/api/mint-wax'),
1933
- * });
1934
- *
1935
- * @example
1936
- * // React
1937
- * <OzElements pubKey="pk_live_..." fetchWaxKey={createFetchWaxKey('/api/mint-wax')}>
1938
- */
1939
- function createFetchWaxKey(mintUrl) {
1940
- const TIMEOUT_MS = 10000;
1941
- // Each attempt gets its own AbortController so a timeout on attempt 1 does
1942
- // not bleed into the retry. Uses AbortController + setTimeout instead of
1943
- // AbortSignal.timeout() to support environments without that API.
1944
- const attemptFetch = (sessionId) => {
1945
- const controller = new AbortController();
1946
- const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
1947
- return fetch(mintUrl, {
1948
- method: 'POST',
1949
- headers: { 'Content-Type': 'application/json' },
1950
- body: JSON.stringify({ sessionId }),
1951
- signal: controller.signal,
1952
- }).finally(() => clearTimeout(timer));
1953
- };
1954
- return async (sessionId) => {
1955
- let res;
1956
- try {
1957
- res = await attemptFetch(sessionId);
1958
- }
1959
- catch (firstErr) {
1960
- // Abort/timeout should not be retried — the server received nothing or
1961
- // we already waited the full timeout duration.
1962
- if (firstErr instanceof Error && (firstErr.name === 'AbortError' || firstErr.name === 'TimeoutError')) {
1963
- throw new OzError(`Wax key mint timed out after ${TIMEOUT_MS / 1000}s (${mintUrl})`, undefined, 'timeout');
1964
- }
1965
- // Pure network error (offline, DNS, connection refused) — retry once
1966
- // after a short pause in case of a transient blip.
1967
- await new Promise(resolve => setTimeout(resolve, 750));
1968
- try {
1969
- res = await attemptFetch(sessionId);
1970
- }
1971
- catch (retryErr) {
1972
- const msg = retryErr instanceof Error ? retryErr.message : 'Network error';
1973
- throw new OzError(`Could not reach wax key mint endpoint (${mintUrl}): ${msg}`, undefined, 'network');
1974
- }
1975
- }
1976
- const data = await res.json().catch(() => ({}));
1977
- if (!res.ok) {
1978
- throw new OzError(typeof data.error === 'string' && data.error
1979
- ? data.error
1980
- : `Wax key mint failed (HTTP ${res.status})`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
1981
- }
1982
- if (typeof data.waxKey !== 'string' || !data.waxKey.trim()) {
1983
- throw new OzError('Mint endpoint response is missing waxKey. Check your /api/mint-wax implementation.', undefined, 'validation');
1984
- }
1985
- return data.waxKey;
1986
- };
1987
- }
1988
-
1989
2004
  const OzContext = react.createContext({
1990
2005
  vault: null,
1991
2006
  initError: null,
@@ -2002,7 +2017,7 @@ const OzContext = react.createContext({
2002
2017
  * All `<OzCardNumber />`, `<OzExpiry />`, and `<OzCvv />` children must be
2003
2018
  * rendered inside this provider.
2004
2019
  */
2005
- function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loadTimeoutMs, onWaxRefresh, onReady, appearance, maxTokenizeCalls, debug, children }) {
2020
+ function OzElements({ sessionUrl, getSessionKey, fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loadTimeoutMs, onSessionRefresh, onWaxRefresh, onReady, appearance, sessionLimit, maxTokenizeCalls, debug, children }) {
2006
2021
  const [vault, setVault] = react.useState(null);
2007
2022
  const [initError, setInitError] = react.useState(null);
2008
2023
  const [mountedCount, setMountedCount] = react.useState(0);
@@ -2010,13 +2025,14 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
2010
2025
  const [tokenizeCount, setTokenizeCount] = react.useState(0);
2011
2026
  const onLoadErrorRef = react.useRef(onLoadError);
2012
2027
  onLoadErrorRef.current = onLoadError;
2013
- const onWaxRefreshRef = react.useRef(onWaxRefresh);
2014
- onWaxRefreshRef.current = onWaxRefresh;
2028
+ const onWaxRefreshRef = react.useRef(onSessionRefresh !== null && onSessionRefresh !== void 0 ? onSessionRefresh : onWaxRefresh);
2029
+ onWaxRefreshRef.current = onSessionRefresh !== null && onSessionRefresh !== void 0 ? onSessionRefresh : onWaxRefresh;
2015
2030
  const onReadyRef = react.useRef(onReady);
2016
2031
  onReadyRef.current = onReady;
2017
- // Keep a ref to fetchWaxKey so changes don't trigger vault recreation
2018
- const fetchWaxKeyRef = react.useRef(fetchWaxKey);
2019
- fetchWaxKeyRef.current = fetchWaxKey;
2032
+ // Keep a ref to the session callback so changes don't trigger vault recreation.
2033
+ // Priority mirrors OzVault.create(): sessionUrl > getSessionKey > fetchWaxKey.
2034
+ const getSessionKeyRef = react.useRef(getSessionKey !== null && getSessionKey !== void 0 ? getSessionKey : fetchWaxKey);
2035
+ getSessionKeyRef.current = getSessionKey !== null && getSessionKey !== void 0 ? getSessionKey : fetchWaxKey;
2020
2036
  const appearanceKey = react.useMemo(() => appearance ? JSON.stringify(appearance) : '', [appearance]);
2021
2037
  const fontsKey = react.useMemo(() => fonts ? JSON.stringify(fonts) : '', [fonts]);
2022
2038
  react.useEffect(() => {
@@ -2043,7 +2059,11 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
2043
2059
  // synchronously rather than waiting for the promise to settle. Without this,
2044
2060
  // two hidden iframes and two window listeners briefly coexist.
2045
2061
  const abortController = new AbortController();
2046
- OzVault.create(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ pubKey, fetchWaxKey: (sessionId) => fetchWaxKeyRef.current(sessionId) }, (frameBaseUrl ? { frameBaseUrl } : {})), (parsedFonts ? { fonts: parsedFonts } : {})), (parsedAppearance ? { appearance: parsedAppearance } : {})), (onLoadErrorRef.current ? { onLoadError: fireLoadError, loadTimeoutMs } : {})), {
2062
+ OzVault.create(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ pubKey }, (sessionUrl
2063
+ ? { sessionUrl }
2064
+ : getSessionKeyRef.current
2065
+ ? { getSessionKey: (sessionId) => getSessionKeyRef.current(sessionId) }
2066
+ : {})), (frameBaseUrl ? { frameBaseUrl } : {})), (parsedFonts ? { fonts: parsedFonts } : {})), (parsedAppearance ? { appearance: parsedAppearance } : {})), (onLoadErrorRef.current ? { onLoadError: fireLoadError, loadTimeoutMs } : {})), {
2047
2067
  // Always install onWaxRefresh internally so we can reset tokenizeCount
2048
2068
  // when any wax key refresh occurs (reactive TTL expiry, post-budget
2049
2069
  // proactive, or visibility-change proactive). Without this the React
@@ -2065,11 +2085,12 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
2065
2085
  // the retry tokenization completes (a full fetchWaxKey + tokenize round-
2066
2086
  // trip separates them), so the count correctly resets to 0 then rises to
2067
2087
  // 1 after the retry notifyTokenize fires.
2068
- onWaxRefresh: () => {
2088
+ onSessionRefresh: () => {
2069
2089
  var _a;
2070
2090
  Promise.resolve().then(() => setTokenizeCount(0));
2071
2091
  (_a = onWaxRefreshRef.current) === null || _a === void 0 ? void 0 : _a.call(onWaxRefreshRef);
2072
- }, onReady: () => { var _a; return (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef); } }), (maxTokenizeCalls !== undefined ? { maxTokenizeCalls } : {})), (debug ? { debug: true } : {})), abortController.signal).then(v => {
2092
+ }, onReady: () => { var _a; return (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef); } }), (sessionLimit !== undefined ? { sessionLimit }
2093
+ : maxTokenizeCalls !== undefined ? { maxTokenizeCalls } : {})), (debug ? { debug: true } : {})), abortController.signal).then(v => {
2073
2094
  if (cancelled) {
2074
2095
  v.destroy();
2075
2096
  return;
@@ -2102,7 +2123,7 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
2102
2123
  setVault(null);
2103
2124
  setInitError(null);
2104
2125
  };
2105
- }, [pubKey, frameBaseUrl, loadTimeoutMs, appearanceKey, fontsKey, maxTokenizeCalls, debug]);
2126
+ }, [pubKey, sessionUrl, frameBaseUrl, loadTimeoutMs, appearanceKey, fontsKey, sessionLimit, maxTokenizeCalls, debug]);
2106
2127
  const notifyMount = react.useCallback(() => setMountedCount(n => n + 1), []);
2107
2128
  const notifyReady = react.useCallback(() => setReadyCount(n => n + 1), []);
2108
2129
  const notifyUnmount = react.useCallback(() => {
@@ -2446,6 +2467,6 @@ exports.OzCardNumber = OzCardNumber;
2446
2467
  exports.OzCvv = OzCvv;
2447
2468
  exports.OzElements = OzElements;
2448
2469
  exports.OzExpiry = OzExpiry;
2449
- exports.createFetchWaxKey = createFetchWaxKey;
2470
+ exports.createFetchWaxKey = createSessionFetcher;
2450
2471
  exports.useOzElements = useOzElements;
2451
2472
  //# sourceMappingURL=index.cjs.js.map