@ozura/elements 1.1.0 → 1.2.0-next.27
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/LICENSE +21 -21
- package/README.md +73 -71
- package/dist/frame/element-frame.js +48 -3
- package/dist/frame/element-frame.js.map +1 -1
- package/dist/frame/tokenizer-frame.html +1 -1
- package/dist/frame/tokenizer-frame.js +113 -52
- package/dist/frame/tokenizer-frame.js.map +1 -1
- package/dist/oz-elements.esm.js +363 -311
- package/dist/oz-elements.esm.js.map +1 -1
- package/dist/oz-elements.umd.js +364 -311
- package/dist/oz-elements.umd.js.map +1 -1
- package/dist/react/frame/elementFrame.d.ts +7 -0
- package/dist/react/frame/tokenizerFrame.d.ts +4 -1
- package/dist/react/index.cjs.js +239 -180
- package/dist/react/index.cjs.js.map +1 -1
- package/dist/react/index.esm.js +238 -180
- package/dist/react/index.esm.js.map +1 -1
- package/dist/react/react/index.d.ts +50 -29
- package/dist/react/sdk/OzElement.d.ts +3 -1
- package/dist/react/sdk/OzVault.d.ts +9 -14
- package/dist/react/sdk/createSessionFetcher.d.ts +29 -0
- package/dist/react/sdk/index.d.ts +6 -26
- package/dist/react/server/index.d.ts +126 -74
- package/dist/react/types/index.d.ts +70 -41
- package/dist/server/frame/elementFrame.d.ts +7 -0
- package/dist/server/frame/tokenizerFrame.d.ts +4 -1
- package/dist/server/index.cjs.js +188 -90
- package/dist/server/index.cjs.js.map +1 -1
- package/dist/server/index.esm.js +187 -91
- package/dist/server/index.esm.js.map +1 -1
- package/dist/server/sdk/OzElement.d.ts +3 -1
- package/dist/server/sdk/OzVault.d.ts +9 -14
- package/dist/server/sdk/createSessionFetcher.d.ts +29 -0
- package/dist/server/sdk/index.d.ts +6 -26
- package/dist/server/server/index.d.ts +126 -74
- package/dist/server/types/index.d.ts +70 -41
- package/dist/types/frame/elementFrame.d.ts +7 -0
- package/dist/types/frame/tokenizerFrame.d.ts +4 -1
- package/dist/types/sdk/OzElement.d.ts +3 -1
- package/dist/types/sdk/OzVault.d.ts +9 -14
- package/dist/types/sdk/createSessionFetcher.d.ts +29 -0
- package/dist/types/sdk/index.d.ts +6 -26
- package/dist/types/server/index.d.ts +126 -74
- package/dist/types/types/index.d.ts +70 -41
- package/package.json +1 -1
package/dist/react/index.cjs.js
CHANGED
|
@@ -326,7 +326,7 @@ function sanitizeOptions(options) {
|
|
|
326
326
|
* it never holds raw card data — all sensitive values live in the iframe.
|
|
327
327
|
*/
|
|
328
328
|
class OzElement {
|
|
329
|
-
constructor(elementType, options, vaultId, frameBaseUrl, fonts = [], appearanceStyle) {
|
|
329
|
+
constructor(elementType, options, vaultId, frameBaseUrl, fonts = [], appearanceStyle, onDestroy) {
|
|
330
330
|
this.iframe = null;
|
|
331
331
|
this._frameWindow = null;
|
|
332
332
|
this._ready = false;
|
|
@@ -342,6 +342,7 @@ class OzElement {
|
|
|
342
342
|
this.fonts = fonts;
|
|
343
343
|
this.appearanceStyle = appearanceStyle;
|
|
344
344
|
this.frameId = `oz-${elementType}-${uuid()}`;
|
|
345
|
+
this._onDestroy = onDestroy;
|
|
345
346
|
}
|
|
346
347
|
/** The element type this proxy represents. */
|
|
347
348
|
get type() {
|
|
@@ -497,9 +498,14 @@ class OzElement {
|
|
|
497
498
|
* and prevents future use. Distinct from `unmount()` which allows re-mounting.
|
|
498
499
|
*/
|
|
499
500
|
destroy() {
|
|
501
|
+
var _a;
|
|
500
502
|
this.unmount();
|
|
501
503
|
this.handlers.clear();
|
|
502
504
|
this._destroyed = true;
|
|
505
|
+
// Notify OzVault so it can prune the stale frameId entry from its elements
|
|
506
|
+
// and completionState maps. Without this, manually calling el.destroy() leaks
|
|
507
|
+
// map entries that grow unboundedly in SPA scenarios with repeated mount/unmount.
|
|
508
|
+
(_a = this._onDestroy) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
503
509
|
}
|
|
504
510
|
// ─── Called by OzVault ───────────────────────────────────────────────────
|
|
505
511
|
/**
|
|
@@ -840,13 +846,116 @@ function validateBilling(billing) {
|
|
|
840
846
|
*/
|
|
841
847
|
const PROTOCOL_VERSION = 1;
|
|
842
848
|
|
|
849
|
+
/**
|
|
850
|
+
* Creates a `getSessionKey` callback for `OzVault.create()` and `<OzElements>`.
|
|
851
|
+
*
|
|
852
|
+
* This is the recommended way to wire the SDK to your backend session endpoint.
|
|
853
|
+
* If you don't need custom headers or auth logic, pass `sessionUrl` directly to
|
|
854
|
+
* `OzVault.create()` or `<OzElements>` — it calls this helper internally.
|
|
855
|
+
*
|
|
856
|
+
* The callback POSTs `{ sessionId }` to `url` and reads `sessionKey` (or the
|
|
857
|
+
* legacy `waxKey`) from the JSON response, so it is compatible with both the
|
|
858
|
+
* new `createSessionMiddleware` and the old `createMintWaxMiddleware` backends.
|
|
859
|
+
*
|
|
860
|
+
* Each call enforces a **10-second timeout**. On pure network failures
|
|
861
|
+
* (offline, DNS, connection refused) the request is retried **once after 750ms**.
|
|
862
|
+
* HTTP 4xx/5xx errors are never retried — they indicate misconfiguration.
|
|
863
|
+
*
|
|
864
|
+
* @param url - Absolute or relative URL of your session endpoint, e.g. `'/api/oz-session'`.
|
|
865
|
+
*
|
|
866
|
+
* @example
|
|
867
|
+
* // Simplest — just pass sessionUrl, no need to call this directly
|
|
868
|
+
* const vault = await OzVault.create({ pubKey: 'pk_live_...', sessionUrl: '/api/oz-session' });
|
|
869
|
+
*
|
|
870
|
+
* @example
|
|
871
|
+
* // Manual — use when you need custom headers
|
|
872
|
+
* const vault = await OzVault.create({
|
|
873
|
+
* pubKey: 'pk_live_...',
|
|
874
|
+
* getSessionKey: createSessionFetcher('/api/oz-session'),
|
|
875
|
+
* });
|
|
876
|
+
*/
|
|
877
|
+
function createSessionFetcher(url) {
|
|
878
|
+
const TIMEOUT_MS = 10000;
|
|
879
|
+
// Each attempt gets its own AbortController so a timeout on attempt 1 does
|
|
880
|
+
// not bleed into the retry.
|
|
881
|
+
const attemptFetch = (sessionId) => {
|
|
882
|
+
const controller = new AbortController();
|
|
883
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
884
|
+
return fetch(url, {
|
|
885
|
+
method: 'POST',
|
|
886
|
+
headers: { 'Content-Type': 'application/json' },
|
|
887
|
+
body: JSON.stringify({ sessionId }),
|
|
888
|
+
signal: controller.signal,
|
|
889
|
+
}).finally(() => clearTimeout(timer));
|
|
890
|
+
};
|
|
891
|
+
return async (sessionId) => {
|
|
892
|
+
let res;
|
|
893
|
+
try {
|
|
894
|
+
res = await attemptFetch(sessionId);
|
|
895
|
+
}
|
|
896
|
+
catch (firstErr) {
|
|
897
|
+
// Timeout/abort — don't retry, we already waited the full duration.
|
|
898
|
+
if (firstErr instanceof Error && (firstErr.name === 'AbortError' || firstErr.name === 'TimeoutError')) {
|
|
899
|
+
throw new OzError(`Session endpoint timed out after ${TIMEOUT_MS / 1000}s (${url})`, undefined, 'timeout');
|
|
900
|
+
}
|
|
901
|
+
// Pure network error — retry once after a short pause.
|
|
902
|
+
await new Promise(resolve => setTimeout(resolve, 750));
|
|
903
|
+
try {
|
|
904
|
+
res = await attemptFetch(sessionId);
|
|
905
|
+
}
|
|
906
|
+
catch (retryErr) {
|
|
907
|
+
const msg = retryErr instanceof Error ? retryErr.message : 'Network error';
|
|
908
|
+
throw new OzError(`Could not reach session endpoint (${url}): ${msg}`, undefined, 'network');
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
// Parse JSON separately from the ok-check so that a non-JSON error body
|
|
912
|
+
// (HTML error page, WAF block, CDN 503) produces the right error code.
|
|
913
|
+
// Previously res.json() was attempted before res.ok was checked; a parse
|
|
914
|
+
// failure on a 5xx HTML body would fall through as {} and produce a
|
|
915
|
+
// misleading 'validation' code when the real cause is a server/network issue.
|
|
916
|
+
let data = {};
|
|
917
|
+
try {
|
|
918
|
+
data = await res.json();
|
|
919
|
+
}
|
|
920
|
+
catch (_a) {
|
|
921
|
+
if (!res.ok) {
|
|
922
|
+
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');
|
|
923
|
+
}
|
|
924
|
+
// HTTP 200 but body isn't JSON — this is a misconfigured session endpoint.
|
|
925
|
+
throw new OzError('Session endpoint returned HTTP 200 but the response body is not valid JSON. Check your /api/oz-session implementation.', undefined, 'validation');
|
|
926
|
+
}
|
|
927
|
+
if (!res.ok) {
|
|
928
|
+
throw new OzError(typeof data.error === 'string' && data.error
|
|
929
|
+
? data.error
|
|
930
|
+
: `Session endpoint returned HTTP ${res.status}`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
|
|
931
|
+
}
|
|
932
|
+
// Accept both new `sessionKey` and legacy `waxKey` for backward compatibility
|
|
933
|
+
// with backends that haven't migrated to createSessionMiddleware yet.
|
|
934
|
+
const key = (typeof data.sessionKey === 'string' ? data.sessionKey : '') ||
|
|
935
|
+
(typeof data.waxKey === 'string' ? data.waxKey : '');
|
|
936
|
+
if (!key.trim()) {
|
|
937
|
+
throw new OzError('Session endpoint response is missing sessionKey. Check your /api/oz-session implementation.', undefined, 'validation');
|
|
938
|
+
}
|
|
939
|
+
return key;
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
|
|
843
943
|
function isCardMetadata(v) {
|
|
844
|
-
|
|
944
|
+
if (!v || typeof v !== 'object')
|
|
945
|
+
return false;
|
|
946
|
+
const r = v;
|
|
947
|
+
return (typeof r.last4 === 'string' &&
|
|
948
|
+
typeof r.brand === 'string' &&
|
|
949
|
+
typeof r.expMonth === 'string' &&
|
|
950
|
+
typeof r.expYear === 'string');
|
|
845
951
|
}
|
|
846
952
|
function isBankAccountMetadata(v) {
|
|
847
|
-
|
|
953
|
+
if (!v || typeof v !== 'object')
|
|
954
|
+
return false;
|
|
955
|
+
const r = v;
|
|
956
|
+
return typeof r.last4 === 'string' && typeof r.routingNumberLast4 === 'string';
|
|
848
957
|
}
|
|
849
|
-
const DEFAULT_FRAME_BASE_URL = "https://
|
|
958
|
+
const DEFAULT_FRAME_BASE_URL = "https://lively-hill-097170c0f.4.azurestaticapps.net";
|
|
850
959
|
/**
|
|
851
960
|
* The main entry point for OzElements. Creates and manages iframe-based
|
|
852
961
|
* card input elements that keep raw card data isolated from the merchant page.
|
|
@@ -854,16 +963,10 @@ const DEFAULT_FRAME_BASE_URL = "https://elements.ozura.com";
|
|
|
854
963
|
* Use the static `OzVault.create()` factory — do not call `new OzVault()` directly.
|
|
855
964
|
*
|
|
856
965
|
* @example
|
|
966
|
+
* // Recommended — pass sessionUrl and let the SDK call your backend automatically
|
|
857
967
|
* const vault = await OzVault.create({
|
|
858
|
-
* pubKey: '
|
|
859
|
-
*
|
|
860
|
-
* // Call your backend — which calls ozura.mintWaxKey() from @ozura/elements/server
|
|
861
|
-
* const { waxKey } = await fetch('/api/mint-wax', {
|
|
862
|
-
* method: 'POST',
|
|
863
|
-
* body: JSON.stringify({ sessionId }),
|
|
864
|
-
* }).then(r => r.json());
|
|
865
|
-
* return waxKey;
|
|
866
|
-
* },
|
|
968
|
+
* pubKey: 'pk_prod_...', // or 'pk_test_...' for test mode
|
|
969
|
+
* sessionUrl: '/api/oz-session', // backend endpoint that calls ozura.createSession()
|
|
867
970
|
* });
|
|
868
971
|
* const cardNum = vault.createElement('cardNumber');
|
|
869
972
|
* cardNum.mount('#card-number');
|
|
@@ -879,7 +982,7 @@ class OzVault {
|
|
|
879
982
|
* @internal
|
|
880
983
|
*/
|
|
881
984
|
constructor(options, waxKey, tokenizationSessionId) {
|
|
882
|
-
var _a, _b, _c, _d;
|
|
985
|
+
var _a, _b, _c, _d, _e;
|
|
883
986
|
this.elements = new Map();
|
|
884
987
|
this.elementsByType = new Map();
|
|
885
988
|
this.bankElementsByType = new Map();
|
|
@@ -913,7 +1016,12 @@ class OzVault {
|
|
|
913
1016
|
this.fonts = (_a = options.fonts) !== null && _a !== void 0 ? _a : [];
|
|
914
1017
|
this.resolvedAppearance = resolveAppearance(options.appearance);
|
|
915
1018
|
this.vaultId = `vault-${uuid()}`;
|
|
916
|
-
|
|
1019
|
+
// sessionLimit takes precedence over legacy maxTokenizeCalls.
|
|
1020
|
+
// null means unlimited — use Infinity so the ">=" check never triggers.
|
|
1021
|
+
const rawLimit = options.sessionLimit !== undefined
|
|
1022
|
+
? options.sessionLimit
|
|
1023
|
+
: ((_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3);
|
|
1024
|
+
this._maxTokenizeCalls = rawLimit === null ? Infinity : rawLimit;
|
|
917
1025
|
this._debug = (_c = options.debug) !== null && _c !== void 0 ? _c : false;
|
|
918
1026
|
this.boundHandleMessage = this.handleMessage.bind(this);
|
|
919
1027
|
window.addEventListener('message', this.boundHandleMessage);
|
|
@@ -929,7 +1037,8 @@ class OzVault {
|
|
|
929
1037
|
}
|
|
930
1038
|
}, timeout);
|
|
931
1039
|
}
|
|
932
|
-
|
|
1040
|
+
// onSessionRefresh takes precedence over legacy onWaxRefresh
|
|
1041
|
+
this._onWaxRefresh = (_e = options.onSessionRefresh) !== null && _e !== void 0 ? _e : options.onWaxRefresh;
|
|
933
1042
|
this._onReady = options.onReady;
|
|
934
1043
|
this.log('vault created', { vaultId: this.vaultId, frameBaseUrl: this.frameBaseUrl, maxTokenizeCalls: this._maxTokenizeCalls });
|
|
935
1044
|
}
|
|
@@ -937,51 +1046,63 @@ class OzVault {
|
|
|
937
1046
|
* Creates and returns a ready `OzVault` instance.
|
|
938
1047
|
*
|
|
939
1048
|
* Internally this:
|
|
940
|
-
* 1. Generates a
|
|
1049
|
+
* 1. Generates a session UUID.
|
|
941
1050
|
* 2. Starts loading the hidden tokenizer iframe immediately.
|
|
942
|
-
* 3.
|
|
943
|
-
*
|
|
944
|
-
*
|
|
1051
|
+
* 3. Fetches a session key from your backend concurrently — either via
|
|
1052
|
+
* `sessionUrl` (simplest), `getSessionKey` (custom headers/auth), or the
|
|
1053
|
+
* deprecated `fetchWaxKey` callback.
|
|
1054
|
+
* 4. Resolves with the vault instance once the session key is stored. The iframe
|
|
945
1055
|
* has been loading the whole time, so `isReady` may already be true or
|
|
946
1056
|
* will fire shortly after.
|
|
947
1057
|
*
|
|
948
1058
|
* The returned vault is ready to create elements immediately. `createToken()`
|
|
949
1059
|
* additionally requires `vault.isReady` (tokenizer iframe loaded).
|
|
950
1060
|
*
|
|
951
|
-
* @throws {OzError} if
|
|
1061
|
+
* @throws {OzError} if the session fetch fails, times out, or returns an empty string.
|
|
952
1062
|
*/
|
|
953
1063
|
static async create(options, signal) {
|
|
954
1064
|
if (!options.pubKey || !options.pubKey.trim()) {
|
|
955
1065
|
throw new OzError('pubKey is required in options. Obtain your public key from the Ozura admin.');
|
|
956
1066
|
}
|
|
957
|
-
|
|
958
|
-
|
|
1067
|
+
// Normalize the session callback. Priority: sessionUrl > getSessionKey > fetchWaxKey (deprecated).
|
|
1068
|
+
// This allows merchants to use the clean new API without touching legacy code.
|
|
1069
|
+
let resolvedFetchKey;
|
|
1070
|
+
if (options.sessionUrl) {
|
|
1071
|
+
resolvedFetchKey = createSessionFetcher(options.sessionUrl);
|
|
1072
|
+
}
|
|
1073
|
+
else if (typeof options.getSessionKey === 'function') {
|
|
1074
|
+
resolvedFetchKey = options.getSessionKey;
|
|
1075
|
+
}
|
|
1076
|
+
else if (typeof options.fetchWaxKey === 'function') {
|
|
1077
|
+
resolvedFetchKey = options.fetchWaxKey;
|
|
1078
|
+
}
|
|
1079
|
+
else {
|
|
1080
|
+
throw new OzError('A session URL or callback is required. Pass sessionUrl, getSessionKey, or fetchWaxKey to OzVault.create().');
|
|
959
1081
|
}
|
|
960
1082
|
const tokenizationSessionId = uuid();
|
|
961
1083
|
// Construct the vault immediately — this mounts the tokenizer iframe so it
|
|
962
|
-
// starts loading while
|
|
963
|
-
// empty and is set below before create() returns.
|
|
1084
|
+
// starts loading while the session fetch is in flight.
|
|
964
1085
|
const vault = new OzVault(options, '', tokenizationSessionId);
|
|
965
1086
|
// If the caller provides an AbortSignal (e.g. React useEffect cleanup),
|
|
966
1087
|
// destroy the vault immediately on abort so the tokenizer iframe and message
|
|
967
|
-
// listener are removed synchronously rather than waiting for
|
|
968
|
-
// settle. This eliminates the brief double-iframe window in React StrictMode.
|
|
1088
|
+
// listener are removed synchronously rather than waiting for the session fetch
|
|
1089
|
+
// to settle. This eliminates the brief double-iframe window in React StrictMode.
|
|
969
1090
|
const onAbort = () => vault.destroy();
|
|
970
1091
|
signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onAbort, { once: true });
|
|
971
1092
|
let waxKey;
|
|
972
1093
|
try {
|
|
973
|
-
waxKey = await
|
|
1094
|
+
waxKey = await resolvedFetchKey(tokenizationSessionId);
|
|
974
1095
|
}
|
|
975
1096
|
catch (err) {
|
|
976
1097
|
signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
|
|
977
1098
|
vault.destroy();
|
|
978
1099
|
if (signal === null || signal === void 0 ? void 0 : signal.aborted)
|
|
979
1100
|
throw new OzError('OzVault.create() was cancelled.');
|
|
980
|
-
// Preserve errorCode/retryable from OzError (e.g. timeout/network from
|
|
1101
|
+
// Preserve errorCode/retryable from OzError (e.g. timeout/network from createSessionFetcher)
|
|
981
1102
|
// so callers can distinguish transient failures from config errors.
|
|
982
1103
|
const originalCode = err instanceof OzError ? err.errorCode : undefined;
|
|
983
1104
|
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
984
|
-
throw new OzError(`
|
|
1105
|
+
throw new OzError(`Session fetch threw an error: ${msg}`, undefined, originalCode);
|
|
985
1106
|
}
|
|
986
1107
|
signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
|
|
987
1108
|
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
@@ -990,11 +1111,11 @@ class OzVault {
|
|
|
990
1111
|
}
|
|
991
1112
|
if (typeof waxKey !== 'string' || !waxKey.trim()) {
|
|
992
1113
|
vault.destroy();
|
|
993
|
-
throw new OzError('
|
|
1114
|
+
throw new OzError('Session fetch returned an empty key. Check your session endpoint response — it must return { sessionKey: "..." }.');
|
|
994
1115
|
}
|
|
995
1116
|
// Static methods can access private fields of instances of the same class.
|
|
996
1117
|
vault.waxKey = waxKey;
|
|
997
|
-
vault._storedFetchWaxKey =
|
|
1118
|
+
vault._storedFetchWaxKey = resolvedFetchKey;
|
|
998
1119
|
// If the tokenizer iframe fired OZ_FRAME_READY before fetchWaxKey resolved,
|
|
999
1120
|
// the OZ_INIT sent at that point had an empty waxKey. Send a follow-up now
|
|
1000
1121
|
// so the tokenizer has the key stored before any createToken() call.
|
|
@@ -1081,7 +1202,12 @@ class OzVault {
|
|
|
1081
1202
|
this.completionState.delete(existing.frameId);
|
|
1082
1203
|
existing.destroy();
|
|
1083
1204
|
}
|
|
1084
|
-
const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance)
|
|
1205
|
+
const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance, () => {
|
|
1206
|
+
// Prune vault-level maps when the element is manually destroyed so they
|
|
1207
|
+
// don't grow unboundedly in SPA scenarios with repeated mount/unmount cycles.
|
|
1208
|
+
this.elements.delete(el.frameId);
|
|
1209
|
+
this.completionState.delete(el.frameId);
|
|
1210
|
+
});
|
|
1085
1211
|
this.elements.set(el.frameId, el);
|
|
1086
1212
|
typeMap.set(type, el);
|
|
1087
1213
|
return el;
|
|
@@ -1660,9 +1786,9 @@ class OzVault {
|
|
|
1660
1786
|
// key without waiting for a vault rejection.
|
|
1661
1787
|
this._tokenizeSuccessCount++;
|
|
1662
1788
|
if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
|
|
1663
|
-
this.log('proactive
|
|
1789
|
+
this.log('proactive session key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
|
|
1664
1790
|
this.refreshWaxKey().catch((err) => {
|
|
1665
|
-
console.warn('[OzVault] Post-budget
|
|
1791
|
+
console.warn('[OzVault] Post-budget session key refresh failed:', err instanceof Error ? err.message : err);
|
|
1666
1792
|
});
|
|
1667
1793
|
}
|
|
1668
1794
|
}
|
|
@@ -1738,61 +1864,65 @@ class OzVault {
|
|
|
1738
1864
|
}
|
|
1739
1865
|
pending.reject(new OzError(normalizeVaultError(raw), raw, errorCode));
|
|
1740
1866
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
const
|
|
1762
|
-
this.
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1867
|
+
else {
|
|
1868
|
+
// Also check bank resolvers — both card and bank errors use OZ_TOKEN_ERROR.
|
|
1869
|
+
// Use else-if rather than sequential checks so a UUID collision (however
|
|
1870
|
+
// improbable) can never trigger double-rejection of two unrelated resolvers.
|
|
1871
|
+
const bankPending = this.bankTokenizeResolvers.get(msg.requestId);
|
|
1872
|
+
if (bankPending) {
|
|
1873
|
+
this.bankTokenizeResolvers.delete(msg.requestId);
|
|
1874
|
+
if (bankPending.timeoutId != null)
|
|
1875
|
+
clearTimeout(bankPending.timeoutId);
|
|
1876
|
+
if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
|
|
1877
|
+
const resetCountAtRetry = this._resetCount;
|
|
1878
|
+
this.refreshWaxKey().then(() => {
|
|
1879
|
+
if (this._destroyed) {
|
|
1880
|
+
bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
if (this._resetCount !== resetCountAtRetry) {
|
|
1884
|
+
bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
const newRequestId = `req-${uuid()}`;
|
|
1888
|
+
this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
|
|
1889
|
+
try {
|
|
1890
|
+
const retryBankChannels = bankPending.readyElements.map(() => new MessageChannel());
|
|
1891
|
+
this.sendToTokenizer({
|
|
1892
|
+
type: 'OZ_BANK_TOKENIZE',
|
|
1893
|
+
requestId: newRequestId,
|
|
1894
|
+
waxKey: this.waxKey,
|
|
1895
|
+
tokenizationSessionId: this.tokenizationSessionId,
|
|
1896
|
+
pubKey: this.pubKey,
|
|
1897
|
+
firstName: bankPending.firstName,
|
|
1898
|
+
lastName: bankPending.lastName,
|
|
1899
|
+
fieldCount: bankPending.fieldCount,
|
|
1900
|
+
}, retryBankChannels.map(ch => ch.port1));
|
|
1901
|
+
bankPending.readyElements.forEach((el, i) => el.beginCollect(newRequestId, retryBankChannels[i].port2));
|
|
1902
|
+
const retryBankTimeoutId = setTimeout(() => {
|
|
1903
|
+
if (this.bankTokenizeResolvers.has(newRequestId)) {
|
|
1904
|
+
this.bankTokenizeResolvers.delete(newRequestId);
|
|
1905
|
+
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId: newRequestId });
|
|
1906
|
+
bankPending.reject(new OzError('Bank tokenization timed out after wax key refresh.', undefined, 'timeout'));
|
|
1907
|
+
}
|
|
1908
|
+
}, 30000);
|
|
1909
|
+
const retryBankEntry = this.bankTokenizeResolvers.get(newRequestId);
|
|
1910
|
+
if (retryBankEntry)
|
|
1911
|
+
retryBankEntry.timeoutId = retryBankTimeoutId;
|
|
1912
|
+
}
|
|
1913
|
+
catch (setupErr) {
|
|
1914
|
+
this.bankTokenizeResolvers.delete(newRequestId);
|
|
1915
|
+
bankPending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry bank tokenization failed to start'));
|
|
1916
|
+
}
|
|
1917
|
+
}).catch((refreshErr) => {
|
|
1918
|
+
const msg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
|
|
1919
|
+
bankPending.reject(new OzError(msg, undefined, 'auth'));
|
|
1920
|
+
});
|
|
1921
|
+
break;
|
|
1922
|
+
}
|
|
1923
|
+
bankPending.reject(new OzError(normalizeBankVaultError(raw), raw, errorCode));
|
|
1793
1924
|
}
|
|
1794
|
-
|
|
1795
|
-
}
|
|
1925
|
+
} // end else (bank path)
|
|
1796
1926
|
break;
|
|
1797
1927
|
}
|
|
1798
1928
|
case 'OZ_BANK_TOKEN_RESULT': {
|
|
@@ -1820,9 +1950,9 @@ class OzVault {
|
|
|
1820
1950
|
// Same proactive refresh logic as card tokenization.
|
|
1821
1951
|
this._tokenizeSuccessCount++;
|
|
1822
1952
|
if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
|
|
1823
|
-
this.log('proactive
|
|
1953
|
+
this.log('proactive session key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
|
|
1824
1954
|
this.refreshWaxKey().catch((err) => {
|
|
1825
|
-
console.warn('[OzVault] Post-budget
|
|
1955
|
+
console.warn('[OzVault] Post-budget session key refresh failed:', err instanceof Error ? err.message : err);
|
|
1826
1956
|
});
|
|
1827
1957
|
}
|
|
1828
1958
|
}
|
|
@@ -1908,84 +2038,6 @@ class OzVault {
|
|
|
1908
2038
|
}
|
|
1909
2039
|
}
|
|
1910
2040
|
|
|
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
2041
|
const OzContext = react.createContext({
|
|
1990
2042
|
vault: null,
|
|
1991
2043
|
initError: null,
|
|
@@ -2002,7 +2054,7 @@ const OzContext = react.createContext({
|
|
|
2002
2054
|
* All `<OzCardNumber />`, `<OzExpiry />`, and `<OzCvv />` children must be
|
|
2003
2055
|
* rendered inside this provider.
|
|
2004
2056
|
*/
|
|
2005
|
-
function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loadTimeoutMs, onWaxRefresh, onReady, appearance, maxTokenizeCalls, debug, children }) {
|
|
2057
|
+
function OzElements({ sessionUrl, getSessionKey, fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loadTimeoutMs, onSessionRefresh, onWaxRefresh, onReady, appearance, sessionLimit, maxTokenizeCalls, debug, children }) {
|
|
2006
2058
|
const [vault, setVault] = react.useState(null);
|
|
2007
2059
|
const [initError, setInitError] = react.useState(null);
|
|
2008
2060
|
const [mountedCount, setMountedCount] = react.useState(0);
|
|
@@ -2010,13 +2062,14 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
|
|
|
2010
2062
|
const [tokenizeCount, setTokenizeCount] = react.useState(0);
|
|
2011
2063
|
const onLoadErrorRef = react.useRef(onLoadError);
|
|
2012
2064
|
onLoadErrorRef.current = onLoadError;
|
|
2013
|
-
const onWaxRefreshRef = react.useRef(onWaxRefresh);
|
|
2014
|
-
onWaxRefreshRef.current = onWaxRefresh;
|
|
2065
|
+
const onWaxRefreshRef = react.useRef(onSessionRefresh !== null && onSessionRefresh !== void 0 ? onSessionRefresh : onWaxRefresh);
|
|
2066
|
+
onWaxRefreshRef.current = onSessionRefresh !== null && onSessionRefresh !== void 0 ? onSessionRefresh : onWaxRefresh;
|
|
2015
2067
|
const onReadyRef = react.useRef(onReady);
|
|
2016
2068
|
onReadyRef.current = onReady;
|
|
2017
|
-
// Keep a ref to
|
|
2018
|
-
|
|
2019
|
-
|
|
2069
|
+
// Keep a ref to the session callback so changes don't trigger vault recreation.
|
|
2070
|
+
// Priority mirrors OzVault.create(): sessionUrl > getSessionKey > fetchWaxKey.
|
|
2071
|
+
const getSessionKeyRef = react.useRef(getSessionKey !== null && getSessionKey !== void 0 ? getSessionKey : fetchWaxKey);
|
|
2072
|
+
getSessionKeyRef.current = getSessionKey !== null && getSessionKey !== void 0 ? getSessionKey : fetchWaxKey;
|
|
2020
2073
|
const appearanceKey = react.useMemo(() => appearance ? JSON.stringify(appearance) : '', [appearance]);
|
|
2021
2074
|
const fontsKey = react.useMemo(() => fonts ? JSON.stringify(fonts) : '', [fonts]);
|
|
2022
2075
|
react.useEffect(() => {
|
|
@@ -2043,7 +2096,11 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
|
|
|
2043
2096
|
// synchronously rather than waiting for the promise to settle. Without this,
|
|
2044
2097
|
// two hidden iframes and two window listeners briefly coexist.
|
|
2045
2098
|
const abortController = new AbortController();
|
|
2046
|
-
OzVault.create(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(
|
|
2099
|
+
OzVault.create(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ pubKey }, (sessionUrl
|
|
2100
|
+
? { sessionUrl }
|
|
2101
|
+
: getSessionKeyRef.current
|
|
2102
|
+
? { getSessionKey: (sessionId) => getSessionKeyRef.current(sessionId) }
|
|
2103
|
+
: {})), (frameBaseUrl ? { frameBaseUrl } : {})), (parsedFonts ? { fonts: parsedFonts } : {})), (parsedAppearance ? { appearance: parsedAppearance } : {})), (onLoadErrorRef.current ? { onLoadError: fireLoadError, loadTimeoutMs } : {})), {
|
|
2047
2104
|
// Always install onWaxRefresh internally so we can reset tokenizeCount
|
|
2048
2105
|
// when any wax key refresh occurs (reactive TTL expiry, post-budget
|
|
2049
2106
|
// proactive, or visibility-change proactive). Without this the React
|
|
@@ -2065,11 +2122,12 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
|
|
|
2065
2122
|
// the retry tokenization completes (a full fetchWaxKey + tokenize round-
|
|
2066
2123
|
// trip separates them), so the count correctly resets to 0 then rises to
|
|
2067
2124
|
// 1 after the retry notifyTokenize fires.
|
|
2068
|
-
|
|
2125
|
+
onSessionRefresh: () => {
|
|
2069
2126
|
var _a;
|
|
2070
2127
|
Promise.resolve().then(() => setTokenizeCount(0));
|
|
2071
2128
|
(_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); } }), (
|
|
2129
|
+
}, onReady: () => { var _a; return (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef); } }), (sessionLimit !== undefined ? { sessionLimit }
|
|
2130
|
+
: maxTokenizeCalls !== undefined ? { maxTokenizeCalls } : {})), (debug ? { debug: true } : {})), abortController.signal).then(v => {
|
|
2073
2131
|
if (cancelled) {
|
|
2074
2132
|
v.destroy();
|
|
2075
2133
|
return;
|
|
@@ -2102,7 +2160,7 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
|
|
|
2102
2160
|
setVault(null);
|
|
2103
2161
|
setInitError(null);
|
|
2104
2162
|
};
|
|
2105
|
-
}, [pubKey, frameBaseUrl, loadTimeoutMs, appearanceKey, fontsKey, maxTokenizeCalls, debug]);
|
|
2163
|
+
}, [pubKey, sessionUrl, frameBaseUrl, loadTimeoutMs, appearanceKey, fontsKey, sessionLimit, maxTokenizeCalls, debug]);
|
|
2106
2164
|
const notifyMount = react.useCallback(() => setMountedCount(n => n + 1), []);
|
|
2107
2165
|
const notifyReady = react.useCallback(() => setReadyCount(n => n + 1), []);
|
|
2108
2166
|
const notifyUnmount = react.useCallback(() => {
|
|
@@ -2446,6 +2504,7 @@ exports.OzCardNumber = OzCardNumber;
|
|
|
2446
2504
|
exports.OzCvv = OzCvv;
|
|
2447
2505
|
exports.OzElements = OzElements;
|
|
2448
2506
|
exports.OzExpiry = OzExpiry;
|
|
2449
|
-
exports.createFetchWaxKey =
|
|
2507
|
+
exports.createFetchWaxKey = createSessionFetcher;
|
|
2508
|
+
exports.createSessionFetcher = createSessionFetcher;
|
|
2450
2509
|
exports.useOzElements = useOzElements;
|
|
2451
2510
|
//# sourceMappingURL=index.cjs.js.map
|