@ozura/elements 1.2.1 → 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 +49 -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 +30 -11
- 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 +65 -24
- package/dist/server/index.cjs.js.map +1 -1
- package/dist/server/index.esm.js +65 -24
- 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 +30 -11
- 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 +30 -11
- 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/oz-elements.esm.js
CHANGED
|
@@ -198,7 +198,7 @@ function normalizeCommonVaultError(msg) {
|
|
|
198
198
|
if (msg.includes('timeout') || msg.includes('timed out')) {
|
|
199
199
|
return 'The request timed out. Please try again.';
|
|
200
200
|
}
|
|
201
|
-
if (msg.includes('http 5') ||
|
|
201
|
+
if (msg.includes('http 5') || /\b5\d{2}\b/.test(msg)) {
|
|
202
202
|
return 'A server error occurred. Please try again shortly.';
|
|
203
203
|
}
|
|
204
204
|
return null;
|
|
@@ -218,7 +218,7 @@ function normalizeVaultError(raw) {
|
|
|
218
218
|
if (msg.includes('cvv') || msg.includes('cvc') || msg.includes('security code')) {
|
|
219
219
|
return 'The CVV code is invalid. Please check and try again.';
|
|
220
220
|
}
|
|
221
|
-
if (msg.includes('insufficient
|
|
221
|
+
if (msg.includes('insufficient funds')) {
|
|
222
222
|
return 'Your card has insufficient funds. Please use a different card.';
|
|
223
223
|
}
|
|
224
224
|
if (msg.includes('declined') || msg.includes('do not honor')) {
|
|
@@ -376,7 +376,7 @@ function sanitizeOptions(options) {
|
|
|
376
376
|
* it never holds raw card data — all sensitive values live in the iframe.
|
|
377
377
|
*/
|
|
378
378
|
class OzElement {
|
|
379
|
-
constructor(elementType, options, vaultId, frameBaseUrl, fonts = [], appearanceStyle) {
|
|
379
|
+
constructor(elementType, options, vaultId, frameBaseUrl, fonts = [], appearanceStyle, onDestroy, debug = false) {
|
|
380
380
|
this.iframe = null;
|
|
381
381
|
this._frameWindow = null;
|
|
382
382
|
this._ready = false;
|
|
@@ -384,6 +384,7 @@ class OzElement {
|
|
|
384
384
|
this._loadTimer = null;
|
|
385
385
|
this.pendingMessages = [];
|
|
386
386
|
this.handlers = new Map();
|
|
387
|
+
this.debug = false;
|
|
387
388
|
this.elementType = elementType;
|
|
388
389
|
this.options = sanitizeOptions(options);
|
|
389
390
|
this.vaultId = vaultId;
|
|
@@ -392,6 +393,8 @@ class OzElement {
|
|
|
392
393
|
this.fonts = fonts;
|
|
393
394
|
this.appearanceStyle = appearanceStyle;
|
|
394
395
|
this.frameId = `oz-${elementType}-${uuid()}`;
|
|
396
|
+
this._onDestroy = onDestroy;
|
|
397
|
+
this.debug = debug;
|
|
395
398
|
}
|
|
396
399
|
/** The element type this proxy represents. */
|
|
397
400
|
get type() {
|
|
@@ -431,11 +434,17 @@ class OzElement {
|
|
|
431
434
|
accountNumber: 'account number',
|
|
432
435
|
routingNumber: 'routing number',
|
|
433
436
|
}[this.elementType]) !== null && _a !== void 0 ? _a : this.elementType} input`;
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
437
|
+
// sandbox="allow-scripts" gives correct iframe isolation:
|
|
438
|
+
// - Scripts run (allow-scripts), so the field JS executes normally.
|
|
439
|
+
// - NO allow-same-origin: the frame cannot access window.parent's DOM,
|
|
440
|
+
// localStorage, or cookies — prevents sandbox escape even if served
|
|
441
|
+
// from the same origin.
|
|
442
|
+
// - NO allow-top-navigation: a rogue/compromised element frame cannot
|
|
443
|
+
// navigate window.top (clickjacking prevention).
|
|
444
|
+
// - NO allow-forms / allow-popups: reduces attack surface.
|
|
445
|
+
// Field values are delivered via postMessage, so no parent access is
|
|
446
|
+
// needed — allow-scripts alone is sufficient.
|
|
447
|
+
iframe.setAttribute('sandbox', 'allow-scripts');
|
|
439
448
|
// Use hash instead of query string — survives clean-URL redirects from static servers.
|
|
440
449
|
// parentOrigin lets the frame target postMessage to the merchant origin instead of '*'.
|
|
441
450
|
const parentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
|
|
@@ -547,9 +556,14 @@ class OzElement {
|
|
|
547
556
|
* and prevents future use. Distinct from `unmount()` which allows re-mounting.
|
|
548
557
|
*/
|
|
549
558
|
destroy() {
|
|
559
|
+
var _a;
|
|
550
560
|
this.unmount();
|
|
551
561
|
this.handlers.clear();
|
|
552
562
|
this._destroyed = true;
|
|
563
|
+
// Notify OzVault so it can prune the stale frameId entry from its elements
|
|
564
|
+
// and completionState maps. Without this, manually calling el.destroy() leaks
|
|
565
|
+
// map entries that grow unboundedly in SPA scenarios with repeated mount/unmount.
|
|
566
|
+
(_a = this._onDestroy) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
553
567
|
}
|
|
554
568
|
// ─── Called by OzVault ───────────────────────────────────────────────────
|
|
555
569
|
/**
|
|
@@ -586,7 +600,7 @@ class OzElement {
|
|
|
586
600
|
}
|
|
587
601
|
this._frameWindow = (_b = (_a = this.iframe) === null || _a === void 0 ? void 0 : _a.contentWindow) !== null && _b !== void 0 ? _b : null;
|
|
588
602
|
const mergedOptions = Object.assign(Object.assign({}, this.options), { style: mergeAppearanceWithElementStyle(this.appearanceStyle, this.options.style) });
|
|
589
|
-
this.post(Object.assign({ type: 'OZ_INIT', elementType: this.elementType, options: sanitizeOptions(mergedOptions), frameId: this.frameId }, (this.fonts.length > 0 ? { fonts: this.fonts } : {})));
|
|
603
|
+
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 } : {})));
|
|
590
604
|
this.pendingMessages.forEach(m => this.send(m));
|
|
591
605
|
this.pendingMessages = [];
|
|
592
606
|
this.emit('ready', undefined);
|
|
@@ -700,13 +714,14 @@ function validateEmail(email) {
|
|
|
700
714
|
// ─── Phone ───────────────────────────────────────────────────────────────────
|
|
701
715
|
/**
|
|
702
716
|
* Validates E.164 phone format: starts with +, 1–3 digit country code,
|
|
703
|
-
* followed by 7–12 digits, total
|
|
717
|
+
* followed by 7–12 digits, max 15 digits total (E.164 spec cap = 16 chars
|
|
718
|
+
* including the leading +).
|
|
704
719
|
*
|
|
705
720
|
* Matches the output of checkout's formatPhoneForAPI() function.
|
|
706
721
|
* Examples: "+15551234567", "+447911123456", "+61412345678"
|
|
707
722
|
*/
|
|
708
723
|
function validateE164Phone(phone) {
|
|
709
|
-
return /^\+[1-9]\d{6,
|
|
724
|
+
return /^\+[1-9]\d{6,14}$/.test(phone);
|
|
710
725
|
}
|
|
711
726
|
// ─── Field length ─────────────────────────────────────────────────────────────
|
|
712
727
|
/** Returns true when the string is non-empty and ≤50 characters (cardSale schema). */
|
|
@@ -909,12 +924,12 @@ const PROTOCOL_VERSION = 1;
|
|
|
909
924
|
*
|
|
910
925
|
* @example
|
|
911
926
|
* // Simplest — just pass sessionUrl, no need to call this directly
|
|
912
|
-
* const vault = await OzVault.create({ pubKey: '
|
|
927
|
+
* const vault = await OzVault.create({ pubKey: 'pk_prod_...', sessionUrl: '/api/oz-session' });
|
|
913
928
|
*
|
|
914
929
|
* @example
|
|
915
930
|
* // Manual — use when you need custom headers
|
|
916
931
|
* const vault = await OzVault.create({
|
|
917
|
-
* pubKey: '
|
|
932
|
+
* pubKey: 'pk_prod_...',
|
|
918
933
|
* getSessionKey: createSessionFetcher('/api/oz-session'),
|
|
919
934
|
* });
|
|
920
935
|
*/
|
|
@@ -952,7 +967,22 @@ function createSessionFetcher(url) {
|
|
|
952
967
|
throw new OzError(`Could not reach session endpoint (${url}): ${msg}`, undefined, 'network');
|
|
953
968
|
}
|
|
954
969
|
}
|
|
955
|
-
|
|
970
|
+
// Parse JSON separately from the ok-check so that a non-JSON error body
|
|
971
|
+
// (HTML error page, WAF block, CDN 503) produces the right error code.
|
|
972
|
+
// Previously res.json() was attempted before res.ok was checked; a parse
|
|
973
|
+
// failure on a 5xx HTML body would fall through as {} and produce a
|
|
974
|
+
// misleading 'validation' code when the real cause is a server/network issue.
|
|
975
|
+
let data = {};
|
|
976
|
+
try {
|
|
977
|
+
data = await res.json();
|
|
978
|
+
}
|
|
979
|
+
catch (_a) {
|
|
980
|
+
if (!res.ok) {
|
|
981
|
+
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');
|
|
982
|
+
}
|
|
983
|
+
// HTTP 200 but body isn't JSON — this is a misconfigured session endpoint.
|
|
984
|
+
throw new OzError('Session endpoint returned HTTP 200 but the response body is not valid JSON. Check your /api/oz-session implementation.', undefined, 'validation');
|
|
985
|
+
}
|
|
956
986
|
if (!res.ok) {
|
|
957
987
|
throw new OzError(typeof data.error === 'string' && data.error
|
|
958
988
|
? data.error
|
|
@@ -970,10 +1000,19 @@ function createSessionFetcher(url) {
|
|
|
970
1000
|
}
|
|
971
1001
|
|
|
972
1002
|
function isCardMetadata(v) {
|
|
973
|
-
|
|
1003
|
+
if (!v || typeof v !== 'object')
|
|
1004
|
+
return false;
|
|
1005
|
+
const r = v;
|
|
1006
|
+
return (typeof r.last4 === 'string' &&
|
|
1007
|
+
typeof r.brand === 'string' &&
|
|
1008
|
+
typeof r.expMonth === 'string' &&
|
|
1009
|
+
typeof r.expYear === 'string');
|
|
974
1010
|
}
|
|
975
1011
|
function isBankAccountMetadata(v) {
|
|
976
|
-
|
|
1012
|
+
if (!v || typeof v !== 'object')
|
|
1013
|
+
return false;
|
|
1014
|
+
const r = v;
|
|
1015
|
+
return typeof r.last4 === 'string' && typeof r.routingNumberLast4 === 'string';
|
|
977
1016
|
}
|
|
978
1017
|
const DEFAULT_FRAME_BASE_URL = "https://elements.ozura.com";
|
|
979
1018
|
/**
|
|
@@ -983,16 +1022,10 @@ const DEFAULT_FRAME_BASE_URL = "https://elements.ozura.com";
|
|
|
983
1022
|
* Use the static `OzVault.create()` factory — do not call `new OzVault()` directly.
|
|
984
1023
|
*
|
|
985
1024
|
* @example
|
|
1025
|
+
* // Recommended — pass sessionUrl and let the SDK call your backend automatically
|
|
986
1026
|
* const vault = await OzVault.create({
|
|
987
|
-
* pubKey: '
|
|
988
|
-
*
|
|
989
|
-
* // Call your backend — which calls ozura.mintWaxKey() from @ozura/elements/server
|
|
990
|
-
* const { waxKey } = await fetch('/api/mint-wax', {
|
|
991
|
-
* method: 'POST',
|
|
992
|
-
* body: JSON.stringify({ sessionId }),
|
|
993
|
-
* }).then(r => r.json());
|
|
994
|
-
* return waxKey;
|
|
995
|
-
* },
|
|
1027
|
+
* pubKey: 'pk_prod_...', // or 'pk_test_...' for test mode
|
|
1028
|
+
* sessionUrl: '/api/oz-session', // backend endpoint that calls ozura.createSession()
|
|
996
1029
|
* });
|
|
997
1030
|
* const cardNum = vault.createElement('cardNumber');
|
|
998
1031
|
* cardNum.mount('#card-number');
|
|
@@ -1038,6 +1071,11 @@ class OzVault {
|
|
|
1038
1071
|
this.tokenizationSessionId = tokenizationSessionId;
|
|
1039
1072
|
this.pubKey = options.pubKey;
|
|
1040
1073
|
this.frameBaseUrl = options.frameBaseUrl || DEFAULT_FRAME_BASE_URL;
|
|
1074
|
+
// Validate immediately after assignment
|
|
1075
|
+
if (!this.frameBaseUrl.startsWith('https://') &&
|
|
1076
|
+
!this.frameBaseUrl.startsWith('http://localhost')) {
|
|
1077
|
+
throw new OzError('frameBaseUrl must use HTTPS (http://localhost is allowed for local development)', undefined, 'config');
|
|
1078
|
+
}
|
|
1041
1079
|
this.frameOrigin = new URL(this.frameBaseUrl).origin;
|
|
1042
1080
|
this.fonts = (_a = options.fonts) !== null && _a !== void 0 ? _a : [];
|
|
1043
1081
|
this.resolvedAppearance = resolveAppearance(options.appearance);
|
|
@@ -1146,7 +1184,7 @@ class OzVault {
|
|
|
1146
1184
|
// the OZ_INIT sent at that point had an empty waxKey. Send a follow-up now
|
|
1147
1185
|
// so the tokenizer has the key stored before any createToken() call.
|
|
1148
1186
|
if (vault.tokenizerReady) {
|
|
1149
|
-
vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey });
|
|
1187
|
+
vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey, debug: vault._debug });
|
|
1150
1188
|
}
|
|
1151
1189
|
vault.log('wax key received — vault ready');
|
|
1152
1190
|
return vault;
|
|
@@ -1228,7 +1266,12 @@ class OzVault {
|
|
|
1228
1266
|
this.completionState.delete(existing.frameId);
|
|
1229
1267
|
existing.destroy();
|
|
1230
1268
|
}
|
|
1231
|
-
const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance)
|
|
1269
|
+
const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance, () => {
|
|
1270
|
+
// Prune vault-level maps when the element is manually destroyed so they
|
|
1271
|
+
// don't grow unboundedly in SPA scenarios with repeated mount/unmount cycles.
|
|
1272
|
+
this.elements.delete(el.frameId);
|
|
1273
|
+
this.completionState.delete(el.frameId);
|
|
1274
|
+
}, this._debug);
|
|
1232
1275
|
this.elements.set(el.frameId, el);
|
|
1233
1276
|
typeMap.set(type, el);
|
|
1234
1277
|
return el;
|
|
@@ -1311,7 +1354,6 @@ class OzVault {
|
|
|
1311
1354
|
this.sendToTokenizer({
|
|
1312
1355
|
type: 'OZ_BANK_TOKENIZE',
|
|
1313
1356
|
requestId,
|
|
1314
|
-
waxKey: this.waxKey,
|
|
1315
1357
|
tokenizationSessionId: this.tokenizationSessionId,
|
|
1316
1358
|
pubKey: this.pubKey,
|
|
1317
1359
|
firstName: options.firstName.trim(),
|
|
@@ -1319,7 +1361,10 @@ class OzVault {
|
|
|
1319
1361
|
fieldCount: readyBankElements.length,
|
|
1320
1362
|
}, bankChannels.map(ch => ch.port1));
|
|
1321
1363
|
this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
|
|
1322
|
-
readyBankElements.forEach((el, i) =>
|
|
1364
|
+
readyBankElements.forEach((el, i) => {
|
|
1365
|
+
this.log('OZ_BEGIN_COLLECT dispatched', { type: el.type, requestIdPrefix: `${requestId.slice(0, 12)}...` });
|
|
1366
|
+
el.beginCollect(requestId, bankChannels[i].port2);
|
|
1367
|
+
});
|
|
1323
1368
|
const bankTimeoutId = setTimeout(() => {
|
|
1324
1369
|
if (this.bankTokenizeResolvers.has(requestId)) {
|
|
1325
1370
|
this.bankTokenizeResolvers.delete(requestId);
|
|
@@ -1403,7 +1448,11 @@ class OzVault {
|
|
|
1403
1448
|
}
|
|
1404
1449
|
this._tokenizing = 'card';
|
|
1405
1450
|
const requestId = `req-${uuid()}`;
|
|
1406
|
-
this.log('createToken() called'
|
|
1451
|
+
this.log('createToken() called', {
|
|
1452
|
+
requestIdPrefix: requestId.slice(0, 12),
|
|
1453
|
+
fields: readyElements.map(el => el.type),
|
|
1454
|
+
billingPresent: Boolean(options.billing),
|
|
1455
|
+
});
|
|
1407
1456
|
return new Promise((resolve, reject) => {
|
|
1408
1457
|
// Capture the reset generation so cleanup() only zeros _tokenizing when it
|
|
1409
1458
|
// still belongs to this invocation — not a newer one that started after a reset.
|
|
@@ -1428,7 +1477,6 @@ class OzVault {
|
|
|
1428
1477
|
this.sendToTokenizer({
|
|
1429
1478
|
type: 'OZ_TOKENIZE',
|
|
1430
1479
|
requestId,
|
|
1431
|
-
waxKey: this.waxKey,
|
|
1432
1480
|
tokenizationSessionId: this.tokenizationSessionId,
|
|
1433
1481
|
pubKey: this.pubKey,
|
|
1434
1482
|
firstName,
|
|
@@ -1441,7 +1489,10 @@ class OzVault {
|
|
|
1441
1489
|
if (cardEntry)
|
|
1442
1490
|
cardEntry.tokenizeStartMs = tokenizeStartMs;
|
|
1443
1491
|
// Tell each ready element frame to send its raw value to the tokenizer
|
|
1444
|
-
readyElements.forEach((el, i) =>
|
|
1492
|
+
readyElements.forEach((el, i) => {
|
|
1493
|
+
this.log('OZ_BEGIN_COLLECT dispatched', { type: el.type, requestIdPrefix: `${requestId.slice(0, 12)}...` });
|
|
1494
|
+
el.beginCollect(requestId, cardChannels[i].port2);
|
|
1495
|
+
});
|
|
1445
1496
|
const cardTimeoutId = setTimeout(() => {
|
|
1446
1497
|
if (this.tokenizeResolvers.has(requestId)) {
|
|
1447
1498
|
this.tokenizeResolvers.delete(requestId);
|
|
@@ -1494,6 +1545,7 @@ class OzVault {
|
|
|
1494
1545
|
this.tokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
|
|
1495
1546
|
if (timeoutId != null)
|
|
1496
1547
|
clearTimeout(timeoutId);
|
|
1548
|
+
this.log('OZ_TOKENIZE_CANCEL sent (reset)', { requestIdPrefix: `${requestId.slice(0, 12)}...` });
|
|
1497
1549
|
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
|
|
1498
1550
|
reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
1499
1551
|
});
|
|
@@ -1501,6 +1553,7 @@ class OzVault {
|
|
|
1501
1553
|
this.bankTokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
|
|
1502
1554
|
if (timeoutId != null)
|
|
1503
1555
|
clearTimeout(timeoutId);
|
|
1556
|
+
this.log('OZ_TOKENIZE_CANCEL sent (reset, bank)', { requestIdPrefix: `${requestId.slice(0, 12)}...` });
|
|
1504
1557
|
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
|
|
1505
1558
|
reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
1506
1559
|
});
|
|
@@ -1675,7 +1728,7 @@ class OzVault {
|
|
|
1675
1728
|
}
|
|
1676
1729
|
}
|
|
1677
1730
|
handleMessage(event) {
|
|
1678
|
-
var _a;
|
|
1731
|
+
var _a, _b;
|
|
1679
1732
|
if (this._destroyed)
|
|
1680
1733
|
return;
|
|
1681
1734
|
// Only accept messages from our frame origin (defense in depth; prevents
|
|
@@ -1690,6 +1743,15 @@ class OzVault {
|
|
|
1690
1743
|
this.handleTokenizerMessage(msg);
|
|
1691
1744
|
return;
|
|
1692
1745
|
}
|
|
1746
|
+
// OZ_TOKEN_ERROR can arrive from element frames when the MessagePort
|
|
1747
|
+
// transferred in OZ_BEGIN_COLLECT was dropped by the browser (e.g. the
|
|
1748
|
+
// frame navigated). These carry a requestId but no frameId — route them
|
|
1749
|
+
// through handleTokenizerMessage so the pending promise is rejected
|
|
1750
|
+
// immediately rather than waiting for the 30 s collect timeout.
|
|
1751
|
+
if (msg.type === 'OZ_TOKEN_ERROR') {
|
|
1752
|
+
this.handleTokenizerMessage(msg);
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1693
1755
|
// Route to the matching element
|
|
1694
1756
|
const frameId = msg.frameId;
|
|
1695
1757
|
if (frameId) {
|
|
@@ -1707,6 +1769,23 @@ class OzVault {
|
|
|
1707
1769
|
}
|
|
1708
1770
|
this.log('element iframe ready', { type: el.type, frameIdPrefix: frameId.slice(0, 8) });
|
|
1709
1771
|
}
|
|
1772
|
+
// Relay debug/warning messages from element iframes into the parent
|
|
1773
|
+
// DevTools console. Element frames run in cross-origin iframes whose
|
|
1774
|
+
// console context is invisible to developers without a frame selector switch.
|
|
1775
|
+
// Errors are always surfaced (genuine failures). Warnings are only emitted
|
|
1776
|
+
// when debug mode is on — CSS var() and font-host warnings fire on every
|
|
1777
|
+
// style update and would pollute production consoles otherwise.
|
|
1778
|
+
if (msg.type === 'OZ_DEBUG_LOG') {
|
|
1779
|
+
const level = typeof msg.level === 'string' ? msg.level : 'warn';
|
|
1780
|
+
const message = typeof msg.message === 'string' ? msg.message : String((_b = msg.message) !== null && _b !== void 0 ? _b : '');
|
|
1781
|
+
if (level === 'error') {
|
|
1782
|
+
console.error(`[OzVault:${el.type}] ${message}`);
|
|
1783
|
+
}
|
|
1784
|
+
else if (this._debug) {
|
|
1785
|
+
console.warn(`[OzVault:${el.type}] ${message}`);
|
|
1786
|
+
}
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1710
1789
|
// Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
|
|
1711
1790
|
if (msg.type === 'OZ_CHANGE') {
|
|
1712
1791
|
this.handleElementChange(msg, el);
|
|
@@ -1751,7 +1830,7 @@ class OzVault {
|
|
|
1751
1830
|
}
|
|
1752
1831
|
}
|
|
1753
1832
|
handleTokenizerMessage(msg) {
|
|
1754
|
-
var _a, _b, _c, _d;
|
|
1833
|
+
var _a, _b, _c, _d, _e;
|
|
1755
1834
|
switch (msg.type) {
|
|
1756
1835
|
case 'OZ_FRAME_READY':
|
|
1757
1836
|
if (msg.__ozVersion !== PROTOCOL_VERSION) {
|
|
@@ -1769,9 +1848,10 @@ class OzVault {
|
|
|
1769
1848
|
// Deliver the wax key via OZ_INIT so the tokenizer stores it internally.
|
|
1770
1849
|
// If waxKey is still empty (fetchWaxKey hasn't resolved yet), it will be
|
|
1771
1850
|
// sent again from create() once the key is available.
|
|
1772
|
-
this.sendToTokenizer(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})));
|
|
1851
|
+
this.sendToTokenizer(Object.assign(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})), { debug: this._debug }));
|
|
1773
1852
|
(_c = this._onReady) === null || _c === void 0 ? void 0 : _c.call(this);
|
|
1774
1853
|
this.log('tokenizer iframe ready', { protocolVersion: (_d = msg.__ozVersion) !== null && _d !== void 0 ? _d : null });
|
|
1854
|
+
this.log('vault state', this.debugState());
|
|
1775
1855
|
break;
|
|
1776
1856
|
case 'OZ_TOKEN_RESULT': {
|
|
1777
1857
|
if (typeof msg.requestId !== 'string' || !msg.requestId) {
|
|
@@ -1797,6 +1877,7 @@ class OzVault {
|
|
|
1797
1877
|
pending.resolve(Object.assign(Object.assign({ token,
|
|
1798
1878
|
cvcSession }, (card ? { card } : {})), (pending.billing ? { billing: pending.billing } : {})));
|
|
1799
1879
|
this.log('token received', {
|
|
1880
|
+
requestIdPrefix: msg.requestId.slice(0, 12),
|
|
1800
1881
|
elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
|
|
1801
1882
|
tokenPresent: true,
|
|
1802
1883
|
cvcSessionPresent: true,
|
|
@@ -1828,7 +1909,7 @@ class OzVault {
|
|
|
1828
1909
|
if (pending.timeoutId != null)
|
|
1829
1910
|
clearTimeout(pending.timeoutId);
|
|
1830
1911
|
const willRefresh = this.isRefreshableAuthError(errorCode, raw) && !pending.retried && Boolean(this._storedFetchWaxKey);
|
|
1831
|
-
this.log('token error', { errorCode, willRefresh });
|
|
1912
|
+
this.log('token error', { requestIdPrefix: msg.requestId.slice(0, 12), errorCode, willRefresh });
|
|
1832
1913
|
// Auto-refresh: if the wax key expired or was consumed and we haven't
|
|
1833
1914
|
// already retried for this request, transparently re-mint and retry.
|
|
1834
1915
|
if (willRefresh) {
|
|
@@ -1846,6 +1927,17 @@ class OzVault {
|
|
|
1846
1927
|
pending.reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
1847
1928
|
return;
|
|
1848
1929
|
}
|
|
1930
|
+
// Verify all elements from the original call are still mounted and
|
|
1931
|
+
// ready. If any were destroyed (e.g. the merchant called
|
|
1932
|
+
// createElement() or destroy() during the async refresh), the
|
|
1933
|
+
// beginCollect() calls below would silently no-op — the tokenizer
|
|
1934
|
+
// would wait forever for field values that never arrive. Reject
|
|
1935
|
+
// immediately with a clear message instead of hanging 30 seconds.
|
|
1936
|
+
const allCardElementsStillReady = pending.readyElements.every(el => this.elements.has(el.frameId) && el.isReady);
|
|
1937
|
+
if (!allCardElementsStillReady) {
|
|
1938
|
+
pending.reject(new OzError('Card fields changed during session refresh. Please re-enter your card details.'));
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1849
1941
|
const newRequestId = `req-${uuid()}`;
|
|
1850
1942
|
// _tokenizing is still 'card' (cleanup() hasn't been called yet)
|
|
1851
1943
|
this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
|
|
@@ -1854,14 +1946,16 @@ class OzVault {
|
|
|
1854
1946
|
this.sendToTokenizer({
|
|
1855
1947
|
type: 'OZ_TOKENIZE',
|
|
1856
1948
|
requestId: newRequestId,
|
|
1857
|
-
waxKey: this.waxKey,
|
|
1858
1949
|
tokenizationSessionId: this.tokenizationSessionId,
|
|
1859
1950
|
pubKey: this.pubKey,
|
|
1860
1951
|
firstName: pending.firstName,
|
|
1861
1952
|
lastName: pending.lastName,
|
|
1862
1953
|
fieldCount: pending.fieldCount,
|
|
1863
1954
|
}, retryCardChannels.map(ch => ch.port1));
|
|
1864
|
-
pending.readyElements.forEach((el, i) =>
|
|
1955
|
+
pending.readyElements.forEach((el, i) => {
|
|
1956
|
+
this.log('OZ_BEGIN_COLLECT dispatched (retry)', { type: el.type, requestIdPrefix: `${newRequestId.slice(0, 12)}...` });
|
|
1957
|
+
el.beginCollect(newRequestId, retryCardChannels[i].port2);
|
|
1958
|
+
});
|
|
1865
1959
|
const retryCardTimeoutId = setTimeout(() => {
|
|
1866
1960
|
if (this.tokenizeResolvers.has(newRequestId)) {
|
|
1867
1961
|
this.tokenizeResolvers.delete(newRequestId);
|
|
@@ -1878,68 +1972,80 @@ class OzVault {
|
|
|
1878
1972
|
pending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry tokenization failed to start'));
|
|
1879
1973
|
}
|
|
1880
1974
|
}).catch((refreshErr) => {
|
|
1881
|
-
const
|
|
1882
|
-
pending.reject(new OzError(
|
|
1975
|
+
const errMsg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
|
|
1976
|
+
pending.reject(new OzError(errMsg, undefined, 'auth'));
|
|
1883
1977
|
});
|
|
1884
1978
|
break;
|
|
1885
1979
|
}
|
|
1886
1980
|
pending.reject(new OzError(normalizeVaultError(raw), raw, errorCode));
|
|
1887
1981
|
}
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
this.
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
this.
|
|
1923
|
-
|
|
1924
|
-
bankPending.
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1982
|
+
else {
|
|
1983
|
+
// Also check bank resolvers — both card and bank errors use OZ_TOKEN_ERROR.
|
|
1984
|
+
// Use else-if rather than sequential checks so a UUID collision (however
|
|
1985
|
+
// improbable) can never trigger double-rejection of two unrelated resolvers.
|
|
1986
|
+
const bankPending = this.bankTokenizeResolvers.get(msg.requestId);
|
|
1987
|
+
if (bankPending) {
|
|
1988
|
+
this.bankTokenizeResolvers.delete(msg.requestId);
|
|
1989
|
+
if (bankPending.timeoutId != null)
|
|
1990
|
+
clearTimeout(bankPending.timeoutId);
|
|
1991
|
+
if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
|
|
1992
|
+
const resetCountAtRetry = this._resetCount;
|
|
1993
|
+
this.refreshWaxKey().then(() => {
|
|
1994
|
+
if (this._destroyed) {
|
|
1995
|
+
bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
if (this._resetCount !== resetCountAtRetry) {
|
|
1999
|
+
bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
// Same stale-element guard as the card retry path above.
|
|
2003
|
+
const allBankElementsStillReady = bankPending.readyElements.every(el => this.elements.has(el.frameId) && el.isReady);
|
|
2004
|
+
if (!allBankElementsStillReady) {
|
|
2005
|
+
bankPending.reject(new OzError('Bank fields changed during session refresh. Please re-enter your account details.'));
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
const newRequestId = `req-${uuid()}`;
|
|
2009
|
+
this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
|
|
2010
|
+
try {
|
|
2011
|
+
const retryBankChannels = bankPending.readyElements.map(() => new MessageChannel());
|
|
2012
|
+
this.sendToTokenizer({
|
|
2013
|
+
type: 'OZ_BANK_TOKENIZE',
|
|
2014
|
+
requestId: newRequestId,
|
|
2015
|
+
tokenizationSessionId: this.tokenizationSessionId,
|
|
2016
|
+
pubKey: this.pubKey,
|
|
2017
|
+
firstName: bankPending.firstName,
|
|
2018
|
+
lastName: bankPending.lastName,
|
|
2019
|
+
fieldCount: bankPending.fieldCount,
|
|
2020
|
+
}, retryBankChannels.map(ch => ch.port1));
|
|
2021
|
+
bankPending.readyElements.forEach((el, i) => {
|
|
2022
|
+
this.log('OZ_BEGIN_COLLECT dispatched (retry)', { type: el.type, requestIdPrefix: `${newRequestId.slice(0, 12)}...` });
|
|
2023
|
+
el.beginCollect(newRequestId, retryBankChannels[i].port2);
|
|
2024
|
+
});
|
|
2025
|
+
const retryBankTimeoutId = setTimeout(() => {
|
|
2026
|
+
if (this.bankTokenizeResolvers.has(newRequestId)) {
|
|
2027
|
+
this.bankTokenizeResolvers.delete(newRequestId);
|
|
2028
|
+
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId: newRequestId });
|
|
2029
|
+
bankPending.reject(new OzError('Bank tokenization timed out after wax key refresh.', undefined, 'timeout'));
|
|
2030
|
+
}
|
|
2031
|
+
}, 30000);
|
|
2032
|
+
const retryBankEntry = this.bankTokenizeResolvers.get(newRequestId);
|
|
2033
|
+
if (retryBankEntry)
|
|
2034
|
+
retryBankEntry.timeoutId = retryBankTimeoutId;
|
|
2035
|
+
}
|
|
2036
|
+
catch (setupErr) {
|
|
2037
|
+
this.bankTokenizeResolvers.delete(newRequestId);
|
|
2038
|
+
bankPending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry bank tokenization failed to start'));
|
|
2039
|
+
}
|
|
2040
|
+
}).catch((refreshErr) => {
|
|
2041
|
+
const errMsg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
|
|
2042
|
+
bankPending.reject(new OzError(errMsg, undefined, 'auth'));
|
|
2043
|
+
});
|
|
2044
|
+
break;
|
|
2045
|
+
}
|
|
2046
|
+
bankPending.reject(new OzError(normalizeBankVaultError(raw), raw, errorCode));
|
|
1940
2047
|
}
|
|
1941
|
-
|
|
1942
|
-
}
|
|
2048
|
+
} // end else (bank path)
|
|
1943
2049
|
break;
|
|
1944
2050
|
}
|
|
1945
2051
|
case 'OZ_BANK_TOKEN_RESULT': {
|
|
@@ -1975,6 +2081,23 @@ class OzVault {
|
|
|
1975
2081
|
}
|
|
1976
2082
|
break;
|
|
1977
2083
|
}
|
|
2084
|
+
case 'OZ_DEBUG_LOG': {
|
|
2085
|
+
// Relay warnings/errors from the tokenizer iframe into the parent page's
|
|
2086
|
+
// DevTools console. The tokenizer runs in a cross-origin iframe whose
|
|
2087
|
+
// console context is invisible to most developers unless they manually
|
|
2088
|
+
// switch the DevTools frame selector.
|
|
2089
|
+
// Errors are always surfaced; warnings only in debug mode to avoid
|
|
2090
|
+
// polluting production consoles.
|
|
2091
|
+
const level = typeof msg.level === 'string' ? msg.level : 'warn';
|
|
2092
|
+
const message = typeof msg.message === 'string' ? msg.message : String((_e = msg.message) !== null && _e !== void 0 ? _e : '');
|
|
2093
|
+
if (level === 'error') {
|
|
2094
|
+
console.error(`[OzVault:tokenizer] ${message}`);
|
|
2095
|
+
}
|
|
2096
|
+
else if (this._debug) {
|
|
2097
|
+
console.warn(`[OzVault:tokenizer] ${message}`);
|
|
2098
|
+
}
|
|
2099
|
+
break;
|
|
2100
|
+
}
|
|
1978
2101
|
}
|
|
1979
2102
|
}
|
|
1980
2103
|
/**
|
|
@@ -2023,6 +2146,7 @@ class OzVault {
|
|
|
2023
2146
|
}
|
|
2024
2147
|
const newSessionId = uuid();
|
|
2025
2148
|
(_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
2149
|
+
const refreshStartMs = Date.now();
|
|
2026
2150
|
this.log('wax key refresh started');
|
|
2027
2151
|
this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
|
|
2028
2152
|
.then(newWaxKey => {
|
|
@@ -2035,12 +2159,12 @@ class OzVault {
|
|
|
2035
2159
|
this._tokenizeSuccessCount = 0;
|
|
2036
2160
|
}
|
|
2037
2161
|
if (!this._destroyed && this.tokenizerReady) {
|
|
2038
|
-
this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey });
|
|
2162
|
+
this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey, debug: this._debug });
|
|
2039
2163
|
}
|
|
2040
|
-
this.log('wax key refresh succeeded');
|
|
2164
|
+
this.log('wax key refresh succeeded', { durationMs: Date.now() - refreshStartMs });
|
|
2041
2165
|
})
|
|
2042
2166
|
.catch((err) => {
|
|
2043
|
-
this.log('wax key refresh failed', { error: err instanceof Error ? err.message : String(err) });
|
|
2167
|
+
this.log('wax key refresh failed', { error: err instanceof Error ? err.message : String(err), durationMs: Date.now() - refreshStartMs });
|
|
2044
2168
|
throw err;
|
|
2045
2169
|
})
|
|
2046
2170
|
.finally(() => {
|