@ozura/elements 1.2.0 → 1.2.3
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 +105 -0
- package/dist/frame/element-frame.js +60 -4
- package/dist/frame/element-frame.js.map +1 -1
- package/dist/frame/tokenizer-frame.js +145 -61
- package/dist/frame/tokenizer-frame.js.map +1 -1
- package/dist/oz-elements.esm.js +221 -97
- package/dist/oz-elements.esm.js.map +1 -1
- package/dist/oz-elements.umd.js +221 -97
- package/dist/oz-elements.umd.js.map +1 -1
- package/dist/react/frame/elementFrame.d.ts +9 -0
- package/dist/react/frame/tokenizerFrame.d.ts +17 -1
- package/dist/react/index.cjs.js +221 -97
- package/dist/react/index.cjs.js.map +1 -1
- package/dist/react/index.esm.js +221 -97
- package/dist/react/index.esm.js.map +1 -1
- package/dist/react/sdk/OzElement.d.ts +4 -1
- package/dist/react/sdk/OzVault.d.ts +3 -9
- package/dist/react/sdk/createSessionFetcher.d.ts +2 -2
- package/dist/react/server/index.d.ts +26 -0
- package/dist/react/types/index.d.ts +37 -14
- package/dist/react/utils/billingUtils.d.ts +2 -1
- package/dist/react/vue/index.d.ts +88 -0
- package/dist/server/frame/elementFrame.d.ts +9 -0
- package/dist/server/frame/tokenizerFrame.d.ts +17 -1
- package/dist/server/index.cjs.js +77 -27
- package/dist/server/index.cjs.js.map +1 -1
- package/dist/server/index.esm.js +77 -27
- package/dist/server/index.esm.js.map +1 -1
- package/dist/server/sdk/OzElement.d.ts +4 -1
- package/dist/server/sdk/OzVault.d.ts +3 -9
- package/dist/server/sdk/createSessionFetcher.d.ts +2 -2
- package/dist/server/server/index.d.ts +26 -0
- package/dist/server/types/index.d.ts +37 -14
- package/dist/server/utils/billingUtils.d.ts +2 -1
- package/dist/server/vue/index.d.ts +88 -0
- package/dist/types/frame/elementFrame.d.ts +9 -0
- package/dist/types/frame/tokenizerFrame.d.ts +17 -1
- package/dist/types/sdk/OzElement.d.ts +4 -1
- package/dist/types/sdk/OzVault.d.ts +3 -9
- package/dist/types/sdk/createSessionFetcher.d.ts +2 -2
- package/dist/types/server/index.d.ts +26 -0
- package/dist/types/types/index.d.ts +37 -14
- package/dist/types/utils/billingUtils.d.ts +2 -1
- package/dist/types/vue/index.d.ts +88 -0
- package/dist/vue/frame/protocol.d.ts +12 -0
- package/dist/vue/index.cjs.js +2335 -0
- package/dist/vue/index.cjs.js.map +1 -0
- package/dist/vue/index.esm.js +2327 -0
- package/dist/vue/index.esm.js.map +1 -0
- package/dist/vue/sdk/OzElement.d.ts +99 -0
- package/dist/vue/sdk/OzVault.d.ts +250 -0
- package/dist/vue/sdk/createSessionFetcher.d.ts +29 -0
- package/dist/vue/sdk/errors.d.ts +65 -0
- package/dist/vue/sdk/index.d.ts +14 -0
- package/dist/vue/types/index.d.ts +667 -0
- package/dist/vue/utils/appearance.d.ts +22 -0
- package/dist/vue/utils/billingUtils.d.ts +61 -0
- package/dist/vue/utils/uuid.d.ts +12 -0
- package/dist/vue/vue/index.d.ts +88 -0
- package/package.json +15 -3
package/dist/react/index.esm.js
CHANGED
|
@@ -201,7 +201,7 @@ function normalizeCommonVaultError(msg) {
|
|
|
201
201
|
if (msg.includes('timeout') || msg.includes('timed out')) {
|
|
202
202
|
return 'The request timed out. Please try again.';
|
|
203
203
|
}
|
|
204
|
-
if (msg.includes('http 5') ||
|
|
204
|
+
if (msg.includes('http 5') || /\b5\d{2}\b/.test(msg)) {
|
|
205
205
|
return 'A server error occurred. Please try again shortly.';
|
|
206
206
|
}
|
|
207
207
|
return null;
|
|
@@ -221,7 +221,7 @@ function normalizeVaultError(raw) {
|
|
|
221
221
|
if (msg.includes('cvv') || msg.includes('cvc') || msg.includes('security code')) {
|
|
222
222
|
return 'The CVV code is invalid. Please check and try again.';
|
|
223
223
|
}
|
|
224
|
-
if (msg.includes('insufficient
|
|
224
|
+
if (msg.includes('insufficient funds')) {
|
|
225
225
|
return 'Your card has insufficient funds. Please use a different card.';
|
|
226
226
|
}
|
|
227
227
|
if (msg.includes('declined') || msg.includes('do not honor')) {
|
|
@@ -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, debug = false) {
|
|
328
328
|
this.iframe = null;
|
|
329
329
|
this._frameWindow = null;
|
|
330
330
|
this._ready = false;
|
|
@@ -332,6 +332,7 @@ class OzElement {
|
|
|
332
332
|
this._loadTimer = null;
|
|
333
333
|
this.pendingMessages = [];
|
|
334
334
|
this.handlers = new Map();
|
|
335
|
+
this.debug = false;
|
|
335
336
|
this.elementType = elementType;
|
|
336
337
|
this.options = sanitizeOptions(options);
|
|
337
338
|
this.vaultId = vaultId;
|
|
@@ -340,6 +341,8 @@ class OzElement {
|
|
|
340
341
|
this.fonts = fonts;
|
|
341
342
|
this.appearanceStyle = appearanceStyle;
|
|
342
343
|
this.frameId = `oz-${elementType}-${uuid()}`;
|
|
344
|
+
this._onDestroy = onDestroy;
|
|
345
|
+
this.debug = debug;
|
|
343
346
|
}
|
|
344
347
|
/** The element type this proxy represents. */
|
|
345
348
|
get type() {
|
|
@@ -379,11 +382,17 @@ class OzElement {
|
|
|
379
382
|
accountNumber: 'account number',
|
|
380
383
|
routingNumber: 'routing number',
|
|
381
384
|
}[this.elementType]) !== null && _a !== void 0 ? _a : this.elementType} input`;
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
385
|
+
// sandbox="allow-scripts" gives correct iframe isolation:
|
|
386
|
+
// - Scripts run (allow-scripts), so the field JS executes normally.
|
|
387
|
+
// - NO allow-same-origin: the frame cannot access window.parent's DOM,
|
|
388
|
+
// localStorage, or cookies — prevents sandbox escape even if served
|
|
389
|
+
// from the same origin.
|
|
390
|
+
// - NO allow-top-navigation: a rogue/compromised element frame cannot
|
|
391
|
+
// navigate window.top (clickjacking prevention).
|
|
392
|
+
// - NO allow-forms / allow-popups: reduces attack surface.
|
|
393
|
+
// Field values are delivered via postMessage, so no parent access is
|
|
394
|
+
// needed — allow-scripts alone is sufficient.
|
|
395
|
+
iframe.setAttribute('sandbox', 'allow-scripts');
|
|
387
396
|
// Use hash instead of query string — survives clean-URL redirects from static servers.
|
|
388
397
|
// parentOrigin lets the frame target postMessage to the merchant origin instead of '*'.
|
|
389
398
|
const parentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
|
|
@@ -495,9 +504,14 @@ class OzElement {
|
|
|
495
504
|
* and prevents future use. Distinct from `unmount()` which allows re-mounting.
|
|
496
505
|
*/
|
|
497
506
|
destroy() {
|
|
507
|
+
var _a;
|
|
498
508
|
this.unmount();
|
|
499
509
|
this.handlers.clear();
|
|
500
510
|
this._destroyed = true;
|
|
511
|
+
// Notify OzVault so it can prune the stale frameId entry from its elements
|
|
512
|
+
// and completionState maps. Without this, manually calling el.destroy() leaks
|
|
513
|
+
// map entries that grow unboundedly in SPA scenarios with repeated mount/unmount.
|
|
514
|
+
(_a = this._onDestroy) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
501
515
|
}
|
|
502
516
|
// ─── Called by OzVault ───────────────────────────────────────────────────
|
|
503
517
|
/**
|
|
@@ -534,7 +548,7 @@ class OzElement {
|
|
|
534
548
|
}
|
|
535
549
|
this._frameWindow = (_b = (_a = this.iframe) === null || _a === void 0 ? void 0 : _a.contentWindow) !== null && _b !== void 0 ? _b : null;
|
|
536
550
|
const mergedOptions = Object.assign(Object.assign({}, this.options), { style: mergeAppearanceWithElementStyle(this.appearanceStyle, this.options.style) });
|
|
537
|
-
this.post(Object.assign({ type: 'OZ_INIT', elementType: this.elementType, options: sanitizeOptions(mergedOptions), frameId: this.frameId }, (this.fonts.length > 0 ? { fonts: this.fonts } : {})));
|
|
551
|
+
this.post(Object.assign({ type: 'OZ_INIT', elementType: this.elementType, options: sanitizeOptions(mergedOptions), frameId: this.frameId, debug: this.debug }, (this.fonts.length > 0 ? { fonts: this.fonts } : {})));
|
|
538
552
|
this.pendingMessages.forEach(m => this.send(m));
|
|
539
553
|
this.pendingMessages = [];
|
|
540
554
|
this.emit('ready', undefined);
|
|
@@ -648,13 +662,14 @@ function validateEmail(email) {
|
|
|
648
662
|
// ─── Phone ───────────────────────────────────────────────────────────────────
|
|
649
663
|
/**
|
|
650
664
|
* Validates E.164 phone format: starts with +, 1–3 digit country code,
|
|
651
|
-
* followed by 7–12 digits, total
|
|
665
|
+
* followed by 7–12 digits, max 15 digits total (E.164 spec cap = 16 chars
|
|
666
|
+
* including the leading +).
|
|
652
667
|
*
|
|
653
668
|
* Matches the output of checkout's formatPhoneForAPI() function.
|
|
654
669
|
* Examples: "+15551234567", "+447911123456", "+61412345678"
|
|
655
670
|
*/
|
|
656
671
|
function validateE164Phone(phone) {
|
|
657
|
-
return /^\+[1-9]\d{6,
|
|
672
|
+
return /^\+[1-9]\d{6,14}$/.test(phone);
|
|
658
673
|
}
|
|
659
674
|
// ─── Field length ─────────────────────────────────────────────────────────────
|
|
660
675
|
/** Returns true when the string is non-empty and ≤50 characters (cardSale schema). */
|
|
@@ -857,12 +872,12 @@ const PROTOCOL_VERSION = 1;
|
|
|
857
872
|
*
|
|
858
873
|
* @example
|
|
859
874
|
* // Simplest — just pass sessionUrl, no need to call this directly
|
|
860
|
-
* const vault = await OzVault.create({ pubKey: '
|
|
875
|
+
* const vault = await OzVault.create({ pubKey: 'pk_prod_...', sessionUrl: '/api/oz-session' });
|
|
861
876
|
*
|
|
862
877
|
* @example
|
|
863
878
|
* // Manual — use when you need custom headers
|
|
864
879
|
* const vault = await OzVault.create({
|
|
865
|
-
* pubKey: '
|
|
880
|
+
* pubKey: 'pk_prod_...',
|
|
866
881
|
* getSessionKey: createSessionFetcher('/api/oz-session'),
|
|
867
882
|
* });
|
|
868
883
|
*/
|
|
@@ -900,7 +915,22 @@ function createSessionFetcher(url) {
|
|
|
900
915
|
throw new OzError(`Could not reach session endpoint (${url}): ${msg}`, undefined, 'network');
|
|
901
916
|
}
|
|
902
917
|
}
|
|
903
|
-
|
|
918
|
+
// Parse JSON separately from the ok-check so that a non-JSON error body
|
|
919
|
+
// (HTML error page, WAF block, CDN 503) produces the right error code.
|
|
920
|
+
// Previously res.json() was attempted before res.ok was checked; a parse
|
|
921
|
+
// failure on a 5xx HTML body would fall through as {} and produce a
|
|
922
|
+
// misleading 'validation' code when the real cause is a server/network issue.
|
|
923
|
+
let data = {};
|
|
924
|
+
try {
|
|
925
|
+
data = await res.json();
|
|
926
|
+
}
|
|
927
|
+
catch (_a) {
|
|
928
|
+
if (!res.ok) {
|
|
929
|
+
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');
|
|
930
|
+
}
|
|
931
|
+
// HTTP 200 but body isn't JSON — this is a misconfigured session endpoint.
|
|
932
|
+
throw new OzError('Session endpoint returned HTTP 200 but the response body is not valid JSON. Check your /api/oz-session implementation.', undefined, 'validation');
|
|
933
|
+
}
|
|
904
934
|
if (!res.ok) {
|
|
905
935
|
throw new OzError(typeof data.error === 'string' && data.error
|
|
906
936
|
? data.error
|
|
@@ -918,10 +948,19 @@ function createSessionFetcher(url) {
|
|
|
918
948
|
}
|
|
919
949
|
|
|
920
950
|
function isCardMetadata(v) {
|
|
921
|
-
|
|
951
|
+
if (!v || typeof v !== 'object')
|
|
952
|
+
return false;
|
|
953
|
+
const r = v;
|
|
954
|
+
return (typeof r.last4 === 'string' &&
|
|
955
|
+
typeof r.brand === 'string' &&
|
|
956
|
+
typeof r.expMonth === 'string' &&
|
|
957
|
+
typeof r.expYear === 'string');
|
|
922
958
|
}
|
|
923
959
|
function isBankAccountMetadata(v) {
|
|
924
|
-
|
|
960
|
+
if (!v || typeof v !== 'object')
|
|
961
|
+
return false;
|
|
962
|
+
const r = v;
|
|
963
|
+
return typeof r.last4 === 'string' && typeof r.routingNumberLast4 === 'string';
|
|
925
964
|
}
|
|
926
965
|
const DEFAULT_FRAME_BASE_URL = "https://elements.ozura.com";
|
|
927
966
|
/**
|
|
@@ -931,16 +970,10 @@ const DEFAULT_FRAME_BASE_URL = "https://elements.ozura.com";
|
|
|
931
970
|
* Use the static `OzVault.create()` factory — do not call `new OzVault()` directly.
|
|
932
971
|
*
|
|
933
972
|
* @example
|
|
973
|
+
* // Recommended — pass sessionUrl and let the SDK call your backend automatically
|
|
934
974
|
* const vault = await OzVault.create({
|
|
935
|
-
* pubKey: '
|
|
936
|
-
*
|
|
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
|
-
* },
|
|
975
|
+
* pubKey: 'pk_prod_...', // or 'pk_test_...' for test mode
|
|
976
|
+
* sessionUrl: '/api/oz-session', // backend endpoint that calls ozura.createSession()
|
|
944
977
|
* });
|
|
945
978
|
* const cardNum = vault.createElement('cardNumber');
|
|
946
979
|
* cardNum.mount('#card-number');
|
|
@@ -986,6 +1019,11 @@ class OzVault {
|
|
|
986
1019
|
this.tokenizationSessionId = tokenizationSessionId;
|
|
987
1020
|
this.pubKey = options.pubKey;
|
|
988
1021
|
this.frameBaseUrl = options.frameBaseUrl || DEFAULT_FRAME_BASE_URL;
|
|
1022
|
+
// Validate immediately after assignment
|
|
1023
|
+
if (!this.frameBaseUrl.startsWith('https://') &&
|
|
1024
|
+
!this.frameBaseUrl.startsWith('http://localhost')) {
|
|
1025
|
+
throw new OzError('frameBaseUrl must use HTTPS (http://localhost is allowed for local development)', undefined, 'config');
|
|
1026
|
+
}
|
|
989
1027
|
this.frameOrigin = new URL(this.frameBaseUrl).origin;
|
|
990
1028
|
this.fonts = (_a = options.fonts) !== null && _a !== void 0 ? _a : [];
|
|
991
1029
|
this.resolvedAppearance = resolveAppearance(options.appearance);
|
|
@@ -1094,7 +1132,7 @@ class OzVault {
|
|
|
1094
1132
|
// the OZ_INIT sent at that point had an empty waxKey. Send a follow-up now
|
|
1095
1133
|
// so the tokenizer has the key stored before any createToken() call.
|
|
1096
1134
|
if (vault.tokenizerReady) {
|
|
1097
|
-
vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey });
|
|
1135
|
+
vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey, debug: vault._debug });
|
|
1098
1136
|
}
|
|
1099
1137
|
vault.log('wax key received — vault ready');
|
|
1100
1138
|
return vault;
|
|
@@ -1176,7 +1214,12 @@ class OzVault {
|
|
|
1176
1214
|
this.completionState.delete(existing.frameId);
|
|
1177
1215
|
existing.destroy();
|
|
1178
1216
|
}
|
|
1179
|
-
const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance)
|
|
1217
|
+
const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance, () => {
|
|
1218
|
+
// Prune vault-level maps when the element is manually destroyed so they
|
|
1219
|
+
// don't grow unboundedly in SPA scenarios with repeated mount/unmount cycles.
|
|
1220
|
+
this.elements.delete(el.frameId);
|
|
1221
|
+
this.completionState.delete(el.frameId);
|
|
1222
|
+
}, this._debug);
|
|
1180
1223
|
this.elements.set(el.frameId, el);
|
|
1181
1224
|
typeMap.set(type, el);
|
|
1182
1225
|
return el;
|
|
@@ -1259,7 +1302,6 @@ class OzVault {
|
|
|
1259
1302
|
this.sendToTokenizer({
|
|
1260
1303
|
type: 'OZ_BANK_TOKENIZE',
|
|
1261
1304
|
requestId,
|
|
1262
|
-
waxKey: this.waxKey,
|
|
1263
1305
|
tokenizationSessionId: this.tokenizationSessionId,
|
|
1264
1306
|
pubKey: this.pubKey,
|
|
1265
1307
|
firstName: options.firstName.trim(),
|
|
@@ -1267,7 +1309,10 @@ class OzVault {
|
|
|
1267
1309
|
fieldCount: readyBankElements.length,
|
|
1268
1310
|
}, bankChannels.map(ch => ch.port1));
|
|
1269
1311
|
this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
|
|
1270
|
-
readyBankElements.forEach((el, i) =>
|
|
1312
|
+
readyBankElements.forEach((el, i) => {
|
|
1313
|
+
this.log('OZ_BEGIN_COLLECT dispatched', { type: el.type, requestIdPrefix: `${requestId.slice(0, 12)}...` });
|
|
1314
|
+
el.beginCollect(requestId, bankChannels[i].port2);
|
|
1315
|
+
});
|
|
1271
1316
|
const bankTimeoutId = setTimeout(() => {
|
|
1272
1317
|
if (this.bankTokenizeResolvers.has(requestId)) {
|
|
1273
1318
|
this.bankTokenizeResolvers.delete(requestId);
|
|
@@ -1351,7 +1396,11 @@ class OzVault {
|
|
|
1351
1396
|
}
|
|
1352
1397
|
this._tokenizing = 'card';
|
|
1353
1398
|
const requestId = `req-${uuid()}`;
|
|
1354
|
-
this.log('createToken() called'
|
|
1399
|
+
this.log('createToken() called', {
|
|
1400
|
+
requestIdPrefix: requestId.slice(0, 12),
|
|
1401
|
+
fields: readyElements.map(el => el.type),
|
|
1402
|
+
billingPresent: Boolean(options.billing),
|
|
1403
|
+
});
|
|
1355
1404
|
return new Promise((resolve, reject) => {
|
|
1356
1405
|
// Capture the reset generation so cleanup() only zeros _tokenizing when it
|
|
1357
1406
|
// still belongs to this invocation — not a newer one that started after a reset.
|
|
@@ -1376,7 +1425,6 @@ class OzVault {
|
|
|
1376
1425
|
this.sendToTokenizer({
|
|
1377
1426
|
type: 'OZ_TOKENIZE',
|
|
1378
1427
|
requestId,
|
|
1379
|
-
waxKey: this.waxKey,
|
|
1380
1428
|
tokenizationSessionId: this.tokenizationSessionId,
|
|
1381
1429
|
pubKey: this.pubKey,
|
|
1382
1430
|
firstName,
|
|
@@ -1389,7 +1437,10 @@ class OzVault {
|
|
|
1389
1437
|
if (cardEntry)
|
|
1390
1438
|
cardEntry.tokenizeStartMs = tokenizeStartMs;
|
|
1391
1439
|
// Tell each ready element frame to send its raw value to the tokenizer
|
|
1392
|
-
readyElements.forEach((el, i) =>
|
|
1440
|
+
readyElements.forEach((el, i) => {
|
|
1441
|
+
this.log('OZ_BEGIN_COLLECT dispatched', { type: el.type, requestIdPrefix: `${requestId.slice(0, 12)}...` });
|
|
1442
|
+
el.beginCollect(requestId, cardChannels[i].port2);
|
|
1443
|
+
});
|
|
1393
1444
|
const cardTimeoutId = setTimeout(() => {
|
|
1394
1445
|
if (this.tokenizeResolvers.has(requestId)) {
|
|
1395
1446
|
this.tokenizeResolvers.delete(requestId);
|
|
@@ -1442,6 +1493,7 @@ class OzVault {
|
|
|
1442
1493
|
this.tokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
|
|
1443
1494
|
if (timeoutId != null)
|
|
1444
1495
|
clearTimeout(timeoutId);
|
|
1496
|
+
this.log('OZ_TOKENIZE_CANCEL sent (reset)', { requestIdPrefix: `${requestId.slice(0, 12)}...` });
|
|
1445
1497
|
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
|
|
1446
1498
|
reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
1447
1499
|
});
|
|
@@ -1449,6 +1501,7 @@ class OzVault {
|
|
|
1449
1501
|
this.bankTokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
|
|
1450
1502
|
if (timeoutId != null)
|
|
1451
1503
|
clearTimeout(timeoutId);
|
|
1504
|
+
this.log('OZ_TOKENIZE_CANCEL sent (reset, bank)', { requestIdPrefix: `${requestId.slice(0, 12)}...` });
|
|
1452
1505
|
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
|
|
1453
1506
|
reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
1454
1507
|
});
|
|
@@ -1623,7 +1676,7 @@ class OzVault {
|
|
|
1623
1676
|
}
|
|
1624
1677
|
}
|
|
1625
1678
|
handleMessage(event) {
|
|
1626
|
-
var _a;
|
|
1679
|
+
var _a, _b;
|
|
1627
1680
|
if (this._destroyed)
|
|
1628
1681
|
return;
|
|
1629
1682
|
// Only accept messages from our frame origin (defense in depth; prevents
|
|
@@ -1638,6 +1691,15 @@ class OzVault {
|
|
|
1638
1691
|
this.handleTokenizerMessage(msg);
|
|
1639
1692
|
return;
|
|
1640
1693
|
}
|
|
1694
|
+
// OZ_TOKEN_ERROR can arrive from element frames when the MessagePort
|
|
1695
|
+
// transferred in OZ_BEGIN_COLLECT was dropped by the browser (e.g. the
|
|
1696
|
+
// frame navigated). These carry a requestId but no frameId — route them
|
|
1697
|
+
// through handleTokenizerMessage so the pending promise is rejected
|
|
1698
|
+
// immediately rather than waiting for the 30 s collect timeout.
|
|
1699
|
+
if (msg.type === 'OZ_TOKEN_ERROR') {
|
|
1700
|
+
this.handleTokenizerMessage(msg);
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1641
1703
|
// Route to the matching element
|
|
1642
1704
|
const frameId = msg.frameId;
|
|
1643
1705
|
if (frameId) {
|
|
@@ -1655,6 +1717,23 @@ class OzVault {
|
|
|
1655
1717
|
}
|
|
1656
1718
|
this.log('element iframe ready', { type: el.type, frameIdPrefix: frameId.slice(0, 8) });
|
|
1657
1719
|
}
|
|
1720
|
+
// Relay debug/warning messages from element iframes into the parent
|
|
1721
|
+
// DevTools console. Element frames run in cross-origin iframes whose
|
|
1722
|
+
// console context is invisible to developers without a frame selector switch.
|
|
1723
|
+
// Errors are always surfaced (genuine failures). Warnings are only emitted
|
|
1724
|
+
// when debug mode is on — CSS var() and font-host warnings fire on every
|
|
1725
|
+
// style update and would pollute production consoles otherwise.
|
|
1726
|
+
if (msg.type === 'OZ_DEBUG_LOG') {
|
|
1727
|
+
const level = typeof msg.level === 'string' ? msg.level : 'warn';
|
|
1728
|
+
const message = typeof msg.message === 'string' ? msg.message : String((_b = msg.message) !== null && _b !== void 0 ? _b : '');
|
|
1729
|
+
if (level === 'error') {
|
|
1730
|
+
console.error(`[OzVault:${el.type}] ${message}`);
|
|
1731
|
+
}
|
|
1732
|
+
else if (this._debug) {
|
|
1733
|
+
console.warn(`[OzVault:${el.type}] ${message}`);
|
|
1734
|
+
}
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1658
1737
|
// Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
|
|
1659
1738
|
if (msg.type === 'OZ_CHANGE') {
|
|
1660
1739
|
this.handleElementChange(msg, el);
|
|
@@ -1699,7 +1778,7 @@ class OzVault {
|
|
|
1699
1778
|
}
|
|
1700
1779
|
}
|
|
1701
1780
|
handleTokenizerMessage(msg) {
|
|
1702
|
-
var _a, _b, _c, _d;
|
|
1781
|
+
var _a, _b, _c, _d, _e;
|
|
1703
1782
|
switch (msg.type) {
|
|
1704
1783
|
case 'OZ_FRAME_READY':
|
|
1705
1784
|
if (msg.__ozVersion !== PROTOCOL_VERSION) {
|
|
@@ -1717,9 +1796,10 @@ class OzVault {
|
|
|
1717
1796
|
// Deliver the wax key via OZ_INIT so the tokenizer stores it internally.
|
|
1718
1797
|
// If waxKey is still empty (fetchWaxKey hasn't resolved yet), it will be
|
|
1719
1798
|
// sent again from create() once the key is available.
|
|
1720
|
-
this.sendToTokenizer(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})));
|
|
1799
|
+
this.sendToTokenizer(Object.assign(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})), { debug: this._debug }));
|
|
1721
1800
|
(_c = this._onReady) === null || _c === void 0 ? void 0 : _c.call(this);
|
|
1722
1801
|
this.log('tokenizer iframe ready', { protocolVersion: (_d = msg.__ozVersion) !== null && _d !== void 0 ? _d : null });
|
|
1802
|
+
this.log('vault state', this.debugState());
|
|
1723
1803
|
break;
|
|
1724
1804
|
case 'OZ_TOKEN_RESULT': {
|
|
1725
1805
|
if (typeof msg.requestId !== 'string' || !msg.requestId) {
|
|
@@ -1745,6 +1825,7 @@ class OzVault {
|
|
|
1745
1825
|
pending.resolve(Object.assign(Object.assign({ token,
|
|
1746
1826
|
cvcSession }, (card ? { card } : {})), (pending.billing ? { billing: pending.billing } : {})));
|
|
1747
1827
|
this.log('token received', {
|
|
1828
|
+
requestIdPrefix: msg.requestId.slice(0, 12),
|
|
1748
1829
|
elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
|
|
1749
1830
|
tokenPresent: true,
|
|
1750
1831
|
cvcSessionPresent: true,
|
|
@@ -1776,7 +1857,7 @@ class OzVault {
|
|
|
1776
1857
|
if (pending.timeoutId != null)
|
|
1777
1858
|
clearTimeout(pending.timeoutId);
|
|
1778
1859
|
const willRefresh = this.isRefreshableAuthError(errorCode, raw) && !pending.retried && Boolean(this._storedFetchWaxKey);
|
|
1779
|
-
this.log('token error', { errorCode, willRefresh });
|
|
1860
|
+
this.log('token error', { requestIdPrefix: msg.requestId.slice(0, 12), errorCode, willRefresh });
|
|
1780
1861
|
// Auto-refresh: if the wax key expired or was consumed and we haven't
|
|
1781
1862
|
// already retried for this request, transparently re-mint and retry.
|
|
1782
1863
|
if (willRefresh) {
|
|
@@ -1794,6 +1875,17 @@ class OzVault {
|
|
|
1794
1875
|
pending.reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
1795
1876
|
return;
|
|
1796
1877
|
}
|
|
1878
|
+
// Verify all elements from the original call are still mounted and
|
|
1879
|
+
// ready. If any were destroyed (e.g. the merchant called
|
|
1880
|
+
// createElement() or destroy() during the async refresh), the
|
|
1881
|
+
// beginCollect() calls below would silently no-op — the tokenizer
|
|
1882
|
+
// would wait forever for field values that never arrive. Reject
|
|
1883
|
+
// immediately with a clear message instead of hanging 30 seconds.
|
|
1884
|
+
const allCardElementsStillReady = pending.readyElements.every(el => this.elements.has(el.frameId) && el.isReady);
|
|
1885
|
+
if (!allCardElementsStillReady) {
|
|
1886
|
+
pending.reject(new OzError('Card fields changed during session refresh. Please re-enter your card details.'));
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1797
1889
|
const newRequestId = `req-${uuid()}`;
|
|
1798
1890
|
// _tokenizing is still 'card' (cleanup() hasn't been called yet)
|
|
1799
1891
|
this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
|
|
@@ -1802,14 +1894,16 @@ class OzVault {
|
|
|
1802
1894
|
this.sendToTokenizer({
|
|
1803
1895
|
type: 'OZ_TOKENIZE',
|
|
1804
1896
|
requestId: newRequestId,
|
|
1805
|
-
waxKey: this.waxKey,
|
|
1806
1897
|
tokenizationSessionId: this.tokenizationSessionId,
|
|
1807
1898
|
pubKey: this.pubKey,
|
|
1808
1899
|
firstName: pending.firstName,
|
|
1809
1900
|
lastName: pending.lastName,
|
|
1810
1901
|
fieldCount: pending.fieldCount,
|
|
1811
1902
|
}, retryCardChannels.map(ch => ch.port1));
|
|
1812
|
-
pending.readyElements.forEach((el, i) =>
|
|
1903
|
+
pending.readyElements.forEach((el, i) => {
|
|
1904
|
+
this.log('OZ_BEGIN_COLLECT dispatched (retry)', { type: el.type, requestIdPrefix: `${newRequestId.slice(0, 12)}...` });
|
|
1905
|
+
el.beginCollect(newRequestId, retryCardChannels[i].port2);
|
|
1906
|
+
});
|
|
1813
1907
|
const retryCardTimeoutId = setTimeout(() => {
|
|
1814
1908
|
if (this.tokenizeResolvers.has(newRequestId)) {
|
|
1815
1909
|
this.tokenizeResolvers.delete(newRequestId);
|
|
@@ -1826,68 +1920,80 @@ class OzVault {
|
|
|
1826
1920
|
pending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry tokenization failed to start'));
|
|
1827
1921
|
}
|
|
1828
1922
|
}).catch((refreshErr) => {
|
|
1829
|
-
const
|
|
1830
|
-
pending.reject(new OzError(
|
|
1923
|
+
const errMsg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
|
|
1924
|
+
pending.reject(new OzError(errMsg, undefined, 'auth'));
|
|
1831
1925
|
});
|
|
1832
1926
|
break;
|
|
1833
1927
|
}
|
|
1834
1928
|
pending.reject(new OzError(normalizeVaultError(raw), raw, errorCode));
|
|
1835
1929
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
this.
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
this.
|
|
1871
|
-
|
|
1872
|
-
bankPending.
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1930
|
+
else {
|
|
1931
|
+
// Also check bank resolvers — both card and bank errors use OZ_TOKEN_ERROR.
|
|
1932
|
+
// Use else-if rather than sequential checks so a UUID collision (however
|
|
1933
|
+
// improbable) can never trigger double-rejection of two unrelated resolvers.
|
|
1934
|
+
const bankPending = this.bankTokenizeResolvers.get(msg.requestId);
|
|
1935
|
+
if (bankPending) {
|
|
1936
|
+
this.bankTokenizeResolvers.delete(msg.requestId);
|
|
1937
|
+
if (bankPending.timeoutId != null)
|
|
1938
|
+
clearTimeout(bankPending.timeoutId);
|
|
1939
|
+
if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
|
|
1940
|
+
const resetCountAtRetry = this._resetCount;
|
|
1941
|
+
this.refreshWaxKey().then(() => {
|
|
1942
|
+
if (this._destroyed) {
|
|
1943
|
+
bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
if (this._resetCount !== resetCountAtRetry) {
|
|
1947
|
+
bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
// Same stale-element guard as the card retry path above.
|
|
1951
|
+
const allBankElementsStillReady = bankPending.readyElements.every(el => this.elements.has(el.frameId) && el.isReady);
|
|
1952
|
+
if (!allBankElementsStillReady) {
|
|
1953
|
+
bankPending.reject(new OzError('Bank fields changed during session refresh. Please re-enter your account details.'));
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
const newRequestId = `req-${uuid()}`;
|
|
1957
|
+
this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
|
|
1958
|
+
try {
|
|
1959
|
+
const retryBankChannels = bankPending.readyElements.map(() => new MessageChannel());
|
|
1960
|
+
this.sendToTokenizer({
|
|
1961
|
+
type: 'OZ_BANK_TOKENIZE',
|
|
1962
|
+
requestId: newRequestId,
|
|
1963
|
+
tokenizationSessionId: this.tokenizationSessionId,
|
|
1964
|
+
pubKey: this.pubKey,
|
|
1965
|
+
firstName: bankPending.firstName,
|
|
1966
|
+
lastName: bankPending.lastName,
|
|
1967
|
+
fieldCount: bankPending.fieldCount,
|
|
1968
|
+
}, retryBankChannels.map(ch => ch.port1));
|
|
1969
|
+
bankPending.readyElements.forEach((el, i) => {
|
|
1970
|
+
this.log('OZ_BEGIN_COLLECT dispatched (retry)', { type: el.type, requestIdPrefix: `${newRequestId.slice(0, 12)}...` });
|
|
1971
|
+
el.beginCollect(newRequestId, retryBankChannels[i].port2);
|
|
1972
|
+
});
|
|
1973
|
+
const retryBankTimeoutId = setTimeout(() => {
|
|
1974
|
+
if (this.bankTokenizeResolvers.has(newRequestId)) {
|
|
1975
|
+
this.bankTokenizeResolvers.delete(newRequestId);
|
|
1976
|
+
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId: newRequestId });
|
|
1977
|
+
bankPending.reject(new OzError('Bank tokenization timed out after wax key refresh.', undefined, 'timeout'));
|
|
1978
|
+
}
|
|
1979
|
+
}, 30000);
|
|
1980
|
+
const retryBankEntry = this.bankTokenizeResolvers.get(newRequestId);
|
|
1981
|
+
if (retryBankEntry)
|
|
1982
|
+
retryBankEntry.timeoutId = retryBankTimeoutId;
|
|
1983
|
+
}
|
|
1984
|
+
catch (setupErr) {
|
|
1985
|
+
this.bankTokenizeResolvers.delete(newRequestId);
|
|
1986
|
+
bankPending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry bank tokenization failed to start'));
|
|
1987
|
+
}
|
|
1988
|
+
}).catch((refreshErr) => {
|
|
1989
|
+
const errMsg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
|
|
1990
|
+
bankPending.reject(new OzError(errMsg, undefined, 'auth'));
|
|
1991
|
+
});
|
|
1992
|
+
break;
|
|
1993
|
+
}
|
|
1994
|
+
bankPending.reject(new OzError(normalizeBankVaultError(raw), raw, errorCode));
|
|
1888
1995
|
}
|
|
1889
|
-
|
|
1890
|
-
}
|
|
1996
|
+
} // end else (bank path)
|
|
1891
1997
|
break;
|
|
1892
1998
|
}
|
|
1893
1999
|
case 'OZ_BANK_TOKEN_RESULT': {
|
|
@@ -1923,6 +2029,23 @@ class OzVault {
|
|
|
1923
2029
|
}
|
|
1924
2030
|
break;
|
|
1925
2031
|
}
|
|
2032
|
+
case 'OZ_DEBUG_LOG': {
|
|
2033
|
+
// Relay warnings/errors from the tokenizer iframe into the parent page's
|
|
2034
|
+
// DevTools console. The tokenizer runs in a cross-origin iframe whose
|
|
2035
|
+
// console context is invisible to most developers unless they manually
|
|
2036
|
+
// switch the DevTools frame selector.
|
|
2037
|
+
// Errors are always surfaced; warnings only in debug mode to avoid
|
|
2038
|
+
// polluting production consoles.
|
|
2039
|
+
const level = typeof msg.level === 'string' ? msg.level : 'warn';
|
|
2040
|
+
const message = typeof msg.message === 'string' ? msg.message : String((_e = msg.message) !== null && _e !== void 0 ? _e : '');
|
|
2041
|
+
if (level === 'error') {
|
|
2042
|
+
console.error(`[OzVault:tokenizer] ${message}`);
|
|
2043
|
+
}
|
|
2044
|
+
else if (this._debug) {
|
|
2045
|
+
console.warn(`[OzVault:tokenizer] ${message}`);
|
|
2046
|
+
}
|
|
2047
|
+
break;
|
|
2048
|
+
}
|
|
1926
2049
|
}
|
|
1927
2050
|
}
|
|
1928
2051
|
/**
|
|
@@ -1971,6 +2094,7 @@ class OzVault {
|
|
|
1971
2094
|
}
|
|
1972
2095
|
const newSessionId = uuid();
|
|
1973
2096
|
(_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
2097
|
+
const refreshStartMs = Date.now();
|
|
1974
2098
|
this.log('wax key refresh started');
|
|
1975
2099
|
this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
|
|
1976
2100
|
.then(newWaxKey => {
|
|
@@ -1983,12 +2107,12 @@ class OzVault {
|
|
|
1983
2107
|
this._tokenizeSuccessCount = 0;
|
|
1984
2108
|
}
|
|
1985
2109
|
if (!this._destroyed && this.tokenizerReady) {
|
|
1986
|
-
this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey });
|
|
2110
|
+
this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey, debug: this._debug });
|
|
1987
2111
|
}
|
|
1988
|
-
this.log('wax key refresh succeeded');
|
|
2112
|
+
this.log('wax key refresh succeeded', { durationMs: Date.now() - refreshStartMs });
|
|
1989
2113
|
})
|
|
1990
2114
|
.catch((err) => {
|
|
1991
|
-
this.log('wax key refresh failed', { error: err instanceof Error ? err.message : String(err) });
|
|
2115
|
+
this.log('wax key refresh failed', { error: err instanceof Error ? err.message : String(err), durationMs: Date.now() - refreshStartMs });
|
|
1992
2116
|
throw err;
|
|
1993
2117
|
})
|
|
1994
2118
|
.finally(() => {
|