@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/oz-elements.umd.js
CHANGED
|
@@ -204,7 +204,7 @@
|
|
|
204
204
|
if (msg.includes('timeout') || msg.includes('timed out')) {
|
|
205
205
|
return 'The request timed out. Please try again.';
|
|
206
206
|
}
|
|
207
|
-
if (msg.includes('http 5') ||
|
|
207
|
+
if (msg.includes('http 5') || /\b5\d{2}\b/.test(msg)) {
|
|
208
208
|
return 'A server error occurred. Please try again shortly.';
|
|
209
209
|
}
|
|
210
210
|
return null;
|
|
@@ -224,7 +224,7 @@
|
|
|
224
224
|
if (msg.includes('cvv') || msg.includes('cvc') || msg.includes('security code')) {
|
|
225
225
|
return 'The CVV code is invalid. Please check and try again.';
|
|
226
226
|
}
|
|
227
|
-
if (msg.includes('insufficient
|
|
227
|
+
if (msg.includes('insufficient funds')) {
|
|
228
228
|
return 'Your card has insufficient funds. Please use a different card.';
|
|
229
229
|
}
|
|
230
230
|
if (msg.includes('declined') || msg.includes('do not honor')) {
|
|
@@ -382,7 +382,7 @@
|
|
|
382
382
|
* it never holds raw card data — all sensitive values live in the iframe.
|
|
383
383
|
*/
|
|
384
384
|
class OzElement {
|
|
385
|
-
constructor(elementType, options, vaultId, frameBaseUrl, fonts = [], appearanceStyle) {
|
|
385
|
+
constructor(elementType, options, vaultId, frameBaseUrl, fonts = [], appearanceStyle, onDestroy, debug = false) {
|
|
386
386
|
this.iframe = null;
|
|
387
387
|
this._frameWindow = null;
|
|
388
388
|
this._ready = false;
|
|
@@ -390,6 +390,7 @@
|
|
|
390
390
|
this._loadTimer = null;
|
|
391
391
|
this.pendingMessages = [];
|
|
392
392
|
this.handlers = new Map();
|
|
393
|
+
this.debug = false;
|
|
393
394
|
this.elementType = elementType;
|
|
394
395
|
this.options = sanitizeOptions(options);
|
|
395
396
|
this.vaultId = vaultId;
|
|
@@ -398,6 +399,8 @@
|
|
|
398
399
|
this.fonts = fonts;
|
|
399
400
|
this.appearanceStyle = appearanceStyle;
|
|
400
401
|
this.frameId = `oz-${elementType}-${uuid()}`;
|
|
402
|
+
this._onDestroy = onDestroy;
|
|
403
|
+
this.debug = debug;
|
|
401
404
|
}
|
|
402
405
|
/** The element type this proxy represents. */
|
|
403
406
|
get type() {
|
|
@@ -437,11 +440,17 @@
|
|
|
437
440
|
accountNumber: 'account number',
|
|
438
441
|
routingNumber: 'routing number',
|
|
439
442
|
}[this.elementType]) !== null && _a !== void 0 ? _a : this.elementType} input`;
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
//
|
|
443
|
+
// sandbox="allow-scripts" gives correct iframe isolation:
|
|
444
|
+
// - Scripts run (allow-scripts), so the field JS executes normally.
|
|
445
|
+
// - NO allow-same-origin: the frame cannot access window.parent's DOM,
|
|
446
|
+
// localStorage, or cookies — prevents sandbox escape even if served
|
|
447
|
+
// from the same origin.
|
|
448
|
+
// - NO allow-top-navigation: a rogue/compromised element frame cannot
|
|
449
|
+
// navigate window.top (clickjacking prevention).
|
|
450
|
+
// - NO allow-forms / allow-popups: reduces attack surface.
|
|
451
|
+
// Field values are delivered via postMessage, so no parent access is
|
|
452
|
+
// needed — allow-scripts alone is sufficient.
|
|
453
|
+
iframe.setAttribute('sandbox', 'allow-scripts');
|
|
445
454
|
// Use hash instead of query string — survives clean-URL redirects from static servers.
|
|
446
455
|
// parentOrigin lets the frame target postMessage to the merchant origin instead of '*'.
|
|
447
456
|
const parentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
|
|
@@ -553,9 +562,14 @@
|
|
|
553
562
|
* and prevents future use. Distinct from `unmount()` which allows re-mounting.
|
|
554
563
|
*/
|
|
555
564
|
destroy() {
|
|
565
|
+
var _a;
|
|
556
566
|
this.unmount();
|
|
557
567
|
this.handlers.clear();
|
|
558
568
|
this._destroyed = true;
|
|
569
|
+
// Notify OzVault so it can prune the stale frameId entry from its elements
|
|
570
|
+
// and completionState maps. Without this, manually calling el.destroy() leaks
|
|
571
|
+
// map entries that grow unboundedly in SPA scenarios with repeated mount/unmount.
|
|
572
|
+
(_a = this._onDestroy) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
559
573
|
}
|
|
560
574
|
// ─── Called by OzVault ───────────────────────────────────────────────────
|
|
561
575
|
/**
|
|
@@ -592,7 +606,7 @@
|
|
|
592
606
|
}
|
|
593
607
|
this._frameWindow = (_b = (_a = this.iframe) === null || _a === void 0 ? void 0 : _a.contentWindow) !== null && _b !== void 0 ? _b : null;
|
|
594
608
|
const mergedOptions = Object.assign(Object.assign({}, this.options), { style: mergeAppearanceWithElementStyle(this.appearanceStyle, this.options.style) });
|
|
595
|
-
this.post(Object.assign({ type: 'OZ_INIT', elementType: this.elementType, options: sanitizeOptions(mergedOptions), frameId: this.frameId }, (this.fonts.length > 0 ? { fonts: this.fonts } : {})));
|
|
609
|
+
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 } : {})));
|
|
596
610
|
this.pendingMessages.forEach(m => this.send(m));
|
|
597
611
|
this.pendingMessages = [];
|
|
598
612
|
this.emit('ready', undefined);
|
|
@@ -706,13 +720,14 @@
|
|
|
706
720
|
// ─── Phone ───────────────────────────────────────────────────────────────────
|
|
707
721
|
/**
|
|
708
722
|
* Validates E.164 phone format: starts with +, 1–3 digit country code,
|
|
709
|
-
* followed by 7–12 digits, total
|
|
723
|
+
* followed by 7–12 digits, max 15 digits total (E.164 spec cap = 16 chars
|
|
724
|
+
* including the leading +).
|
|
710
725
|
*
|
|
711
726
|
* Matches the output of checkout's formatPhoneForAPI() function.
|
|
712
727
|
* Examples: "+15551234567", "+447911123456", "+61412345678"
|
|
713
728
|
*/
|
|
714
729
|
function validateE164Phone(phone) {
|
|
715
|
-
return /^\+[1-9]\d{6,
|
|
730
|
+
return /^\+[1-9]\d{6,14}$/.test(phone);
|
|
716
731
|
}
|
|
717
732
|
// ─── Field length ─────────────────────────────────────────────────────────────
|
|
718
733
|
/** Returns true when the string is non-empty and ≤50 characters (cardSale schema). */
|
|
@@ -915,12 +930,12 @@
|
|
|
915
930
|
*
|
|
916
931
|
* @example
|
|
917
932
|
* // Simplest — just pass sessionUrl, no need to call this directly
|
|
918
|
-
* const vault = await OzVault.create({ pubKey: '
|
|
933
|
+
* const vault = await OzVault.create({ pubKey: 'pk_prod_...', sessionUrl: '/api/oz-session' });
|
|
919
934
|
*
|
|
920
935
|
* @example
|
|
921
936
|
* // Manual — use when you need custom headers
|
|
922
937
|
* const vault = await OzVault.create({
|
|
923
|
-
* pubKey: '
|
|
938
|
+
* pubKey: 'pk_prod_...',
|
|
924
939
|
* getSessionKey: createSessionFetcher('/api/oz-session'),
|
|
925
940
|
* });
|
|
926
941
|
*/
|
|
@@ -958,7 +973,22 @@
|
|
|
958
973
|
throw new OzError(`Could not reach session endpoint (${url}): ${msg}`, undefined, 'network');
|
|
959
974
|
}
|
|
960
975
|
}
|
|
961
|
-
|
|
976
|
+
// Parse JSON separately from the ok-check so that a non-JSON error body
|
|
977
|
+
// (HTML error page, WAF block, CDN 503) produces the right error code.
|
|
978
|
+
// Previously res.json() was attempted before res.ok was checked; a parse
|
|
979
|
+
// failure on a 5xx HTML body would fall through as {} and produce a
|
|
980
|
+
// misleading 'validation' code when the real cause is a server/network issue.
|
|
981
|
+
let data = {};
|
|
982
|
+
try {
|
|
983
|
+
data = await res.json();
|
|
984
|
+
}
|
|
985
|
+
catch (_a) {
|
|
986
|
+
if (!res.ok) {
|
|
987
|
+
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');
|
|
988
|
+
}
|
|
989
|
+
// HTTP 200 but body isn't JSON — this is a misconfigured session endpoint.
|
|
990
|
+
throw new OzError('Session endpoint returned HTTP 200 but the response body is not valid JSON. Check your /api/oz-session implementation.', undefined, 'validation');
|
|
991
|
+
}
|
|
962
992
|
if (!res.ok) {
|
|
963
993
|
throw new OzError(typeof data.error === 'string' && data.error
|
|
964
994
|
? data.error
|
|
@@ -976,10 +1006,19 @@
|
|
|
976
1006
|
}
|
|
977
1007
|
|
|
978
1008
|
function isCardMetadata(v) {
|
|
979
|
-
|
|
1009
|
+
if (!v || typeof v !== 'object')
|
|
1010
|
+
return false;
|
|
1011
|
+
const r = v;
|
|
1012
|
+
return (typeof r.last4 === 'string' &&
|
|
1013
|
+
typeof r.brand === 'string' &&
|
|
1014
|
+
typeof r.expMonth === 'string' &&
|
|
1015
|
+
typeof r.expYear === 'string');
|
|
980
1016
|
}
|
|
981
1017
|
function isBankAccountMetadata(v) {
|
|
982
|
-
|
|
1018
|
+
if (!v || typeof v !== 'object')
|
|
1019
|
+
return false;
|
|
1020
|
+
const r = v;
|
|
1021
|
+
return typeof r.last4 === 'string' && typeof r.routingNumberLast4 === 'string';
|
|
983
1022
|
}
|
|
984
1023
|
const DEFAULT_FRAME_BASE_URL = "https://elements.ozura.com";
|
|
985
1024
|
/**
|
|
@@ -989,16 +1028,10 @@
|
|
|
989
1028
|
* Use the static `OzVault.create()` factory — do not call `new OzVault()` directly.
|
|
990
1029
|
*
|
|
991
1030
|
* @example
|
|
1031
|
+
* // Recommended — pass sessionUrl and let the SDK call your backend automatically
|
|
992
1032
|
* const vault = await OzVault.create({
|
|
993
|
-
* pubKey: '
|
|
994
|
-
*
|
|
995
|
-
* // Call your backend — which calls ozura.mintWaxKey() from @ozura/elements/server
|
|
996
|
-
* const { waxKey } = await fetch('/api/mint-wax', {
|
|
997
|
-
* method: 'POST',
|
|
998
|
-
* body: JSON.stringify({ sessionId }),
|
|
999
|
-
* }).then(r => r.json());
|
|
1000
|
-
* return waxKey;
|
|
1001
|
-
* },
|
|
1033
|
+
* pubKey: 'pk_prod_...', // or 'pk_test_...' for test mode
|
|
1034
|
+
* sessionUrl: '/api/oz-session', // backend endpoint that calls ozura.createSession()
|
|
1002
1035
|
* });
|
|
1003
1036
|
* const cardNum = vault.createElement('cardNumber');
|
|
1004
1037
|
* cardNum.mount('#card-number');
|
|
@@ -1044,6 +1077,11 @@
|
|
|
1044
1077
|
this.tokenizationSessionId = tokenizationSessionId;
|
|
1045
1078
|
this.pubKey = options.pubKey;
|
|
1046
1079
|
this.frameBaseUrl = options.frameBaseUrl || DEFAULT_FRAME_BASE_URL;
|
|
1080
|
+
// Validate immediately after assignment
|
|
1081
|
+
if (!this.frameBaseUrl.startsWith('https://') &&
|
|
1082
|
+
!this.frameBaseUrl.startsWith('http://localhost')) {
|
|
1083
|
+
throw new OzError('frameBaseUrl must use HTTPS (http://localhost is allowed for local development)', undefined, 'config');
|
|
1084
|
+
}
|
|
1047
1085
|
this.frameOrigin = new URL(this.frameBaseUrl).origin;
|
|
1048
1086
|
this.fonts = (_a = options.fonts) !== null && _a !== void 0 ? _a : [];
|
|
1049
1087
|
this.resolvedAppearance = resolveAppearance(options.appearance);
|
|
@@ -1152,7 +1190,7 @@
|
|
|
1152
1190
|
// the OZ_INIT sent at that point had an empty waxKey. Send a follow-up now
|
|
1153
1191
|
// so the tokenizer has the key stored before any createToken() call.
|
|
1154
1192
|
if (vault.tokenizerReady) {
|
|
1155
|
-
vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey });
|
|
1193
|
+
vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey, debug: vault._debug });
|
|
1156
1194
|
}
|
|
1157
1195
|
vault.log('wax key received — vault ready');
|
|
1158
1196
|
return vault;
|
|
@@ -1234,7 +1272,12 @@
|
|
|
1234
1272
|
this.completionState.delete(existing.frameId);
|
|
1235
1273
|
existing.destroy();
|
|
1236
1274
|
}
|
|
1237
|
-
const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance)
|
|
1275
|
+
const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance, () => {
|
|
1276
|
+
// Prune vault-level maps when the element is manually destroyed so they
|
|
1277
|
+
// don't grow unboundedly in SPA scenarios with repeated mount/unmount cycles.
|
|
1278
|
+
this.elements.delete(el.frameId);
|
|
1279
|
+
this.completionState.delete(el.frameId);
|
|
1280
|
+
}, this._debug);
|
|
1238
1281
|
this.elements.set(el.frameId, el);
|
|
1239
1282
|
typeMap.set(type, el);
|
|
1240
1283
|
return el;
|
|
@@ -1317,7 +1360,6 @@
|
|
|
1317
1360
|
this.sendToTokenizer({
|
|
1318
1361
|
type: 'OZ_BANK_TOKENIZE',
|
|
1319
1362
|
requestId,
|
|
1320
|
-
waxKey: this.waxKey,
|
|
1321
1363
|
tokenizationSessionId: this.tokenizationSessionId,
|
|
1322
1364
|
pubKey: this.pubKey,
|
|
1323
1365
|
firstName: options.firstName.trim(),
|
|
@@ -1325,7 +1367,10 @@
|
|
|
1325
1367
|
fieldCount: readyBankElements.length,
|
|
1326
1368
|
}, bankChannels.map(ch => ch.port1));
|
|
1327
1369
|
this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
|
|
1328
|
-
readyBankElements.forEach((el, i) =>
|
|
1370
|
+
readyBankElements.forEach((el, i) => {
|
|
1371
|
+
this.log('OZ_BEGIN_COLLECT dispatched', { type: el.type, requestIdPrefix: `${requestId.slice(0, 12)}...` });
|
|
1372
|
+
el.beginCollect(requestId, bankChannels[i].port2);
|
|
1373
|
+
});
|
|
1329
1374
|
const bankTimeoutId = setTimeout(() => {
|
|
1330
1375
|
if (this.bankTokenizeResolvers.has(requestId)) {
|
|
1331
1376
|
this.bankTokenizeResolvers.delete(requestId);
|
|
@@ -1409,7 +1454,11 @@
|
|
|
1409
1454
|
}
|
|
1410
1455
|
this._tokenizing = 'card';
|
|
1411
1456
|
const requestId = `req-${uuid()}`;
|
|
1412
|
-
this.log('createToken() called'
|
|
1457
|
+
this.log('createToken() called', {
|
|
1458
|
+
requestIdPrefix: requestId.slice(0, 12),
|
|
1459
|
+
fields: readyElements.map(el => el.type),
|
|
1460
|
+
billingPresent: Boolean(options.billing),
|
|
1461
|
+
});
|
|
1413
1462
|
return new Promise((resolve, reject) => {
|
|
1414
1463
|
// Capture the reset generation so cleanup() only zeros _tokenizing when it
|
|
1415
1464
|
// still belongs to this invocation — not a newer one that started after a reset.
|
|
@@ -1434,7 +1483,6 @@
|
|
|
1434
1483
|
this.sendToTokenizer({
|
|
1435
1484
|
type: 'OZ_TOKENIZE',
|
|
1436
1485
|
requestId,
|
|
1437
|
-
waxKey: this.waxKey,
|
|
1438
1486
|
tokenizationSessionId: this.tokenizationSessionId,
|
|
1439
1487
|
pubKey: this.pubKey,
|
|
1440
1488
|
firstName,
|
|
@@ -1447,7 +1495,10 @@
|
|
|
1447
1495
|
if (cardEntry)
|
|
1448
1496
|
cardEntry.tokenizeStartMs = tokenizeStartMs;
|
|
1449
1497
|
// Tell each ready element frame to send its raw value to the tokenizer
|
|
1450
|
-
readyElements.forEach((el, i) =>
|
|
1498
|
+
readyElements.forEach((el, i) => {
|
|
1499
|
+
this.log('OZ_BEGIN_COLLECT dispatched', { type: el.type, requestIdPrefix: `${requestId.slice(0, 12)}...` });
|
|
1500
|
+
el.beginCollect(requestId, cardChannels[i].port2);
|
|
1501
|
+
});
|
|
1451
1502
|
const cardTimeoutId = setTimeout(() => {
|
|
1452
1503
|
if (this.tokenizeResolvers.has(requestId)) {
|
|
1453
1504
|
this.tokenizeResolvers.delete(requestId);
|
|
@@ -1500,6 +1551,7 @@
|
|
|
1500
1551
|
this.tokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
|
|
1501
1552
|
if (timeoutId != null)
|
|
1502
1553
|
clearTimeout(timeoutId);
|
|
1554
|
+
this.log('OZ_TOKENIZE_CANCEL sent (reset)', { requestIdPrefix: `${requestId.slice(0, 12)}...` });
|
|
1503
1555
|
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
|
|
1504
1556
|
reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
1505
1557
|
});
|
|
@@ -1507,6 +1559,7 @@
|
|
|
1507
1559
|
this.bankTokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
|
|
1508
1560
|
if (timeoutId != null)
|
|
1509
1561
|
clearTimeout(timeoutId);
|
|
1562
|
+
this.log('OZ_TOKENIZE_CANCEL sent (reset, bank)', { requestIdPrefix: `${requestId.slice(0, 12)}...` });
|
|
1510
1563
|
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
|
|
1511
1564
|
reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
1512
1565
|
});
|
|
@@ -1681,7 +1734,7 @@
|
|
|
1681
1734
|
}
|
|
1682
1735
|
}
|
|
1683
1736
|
handleMessage(event) {
|
|
1684
|
-
var _a;
|
|
1737
|
+
var _a, _b;
|
|
1685
1738
|
if (this._destroyed)
|
|
1686
1739
|
return;
|
|
1687
1740
|
// Only accept messages from our frame origin (defense in depth; prevents
|
|
@@ -1696,6 +1749,15 @@
|
|
|
1696
1749
|
this.handleTokenizerMessage(msg);
|
|
1697
1750
|
return;
|
|
1698
1751
|
}
|
|
1752
|
+
// OZ_TOKEN_ERROR can arrive from element frames when the MessagePort
|
|
1753
|
+
// transferred in OZ_BEGIN_COLLECT was dropped by the browser (e.g. the
|
|
1754
|
+
// frame navigated). These carry a requestId but no frameId — route them
|
|
1755
|
+
// through handleTokenizerMessage so the pending promise is rejected
|
|
1756
|
+
// immediately rather than waiting for the 30 s collect timeout.
|
|
1757
|
+
if (msg.type === 'OZ_TOKEN_ERROR') {
|
|
1758
|
+
this.handleTokenizerMessage(msg);
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1699
1761
|
// Route to the matching element
|
|
1700
1762
|
const frameId = msg.frameId;
|
|
1701
1763
|
if (frameId) {
|
|
@@ -1713,6 +1775,23 @@
|
|
|
1713
1775
|
}
|
|
1714
1776
|
this.log('element iframe ready', { type: el.type, frameIdPrefix: frameId.slice(0, 8) });
|
|
1715
1777
|
}
|
|
1778
|
+
// Relay debug/warning messages from element iframes into the parent
|
|
1779
|
+
// DevTools console. Element frames run in cross-origin iframes whose
|
|
1780
|
+
// console context is invisible to developers without a frame selector switch.
|
|
1781
|
+
// Errors are always surfaced (genuine failures). Warnings are only emitted
|
|
1782
|
+
// when debug mode is on — CSS var() and font-host warnings fire on every
|
|
1783
|
+
// style update and would pollute production consoles otherwise.
|
|
1784
|
+
if (msg.type === 'OZ_DEBUG_LOG') {
|
|
1785
|
+
const level = typeof msg.level === 'string' ? msg.level : 'warn';
|
|
1786
|
+
const message = typeof msg.message === 'string' ? msg.message : String((_b = msg.message) !== null && _b !== void 0 ? _b : '');
|
|
1787
|
+
if (level === 'error') {
|
|
1788
|
+
console.error(`[OzVault:${el.type}] ${message}`);
|
|
1789
|
+
}
|
|
1790
|
+
else if (this._debug) {
|
|
1791
|
+
console.warn(`[OzVault:${el.type}] ${message}`);
|
|
1792
|
+
}
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1716
1795
|
// Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
|
|
1717
1796
|
if (msg.type === 'OZ_CHANGE') {
|
|
1718
1797
|
this.handleElementChange(msg, el);
|
|
@@ -1757,7 +1836,7 @@
|
|
|
1757
1836
|
}
|
|
1758
1837
|
}
|
|
1759
1838
|
handleTokenizerMessage(msg) {
|
|
1760
|
-
var _a, _b, _c, _d;
|
|
1839
|
+
var _a, _b, _c, _d, _e;
|
|
1761
1840
|
switch (msg.type) {
|
|
1762
1841
|
case 'OZ_FRAME_READY':
|
|
1763
1842
|
if (msg.__ozVersion !== PROTOCOL_VERSION) {
|
|
@@ -1775,9 +1854,10 @@
|
|
|
1775
1854
|
// Deliver the wax key via OZ_INIT so the tokenizer stores it internally.
|
|
1776
1855
|
// If waxKey is still empty (fetchWaxKey hasn't resolved yet), it will be
|
|
1777
1856
|
// sent again from create() once the key is available.
|
|
1778
|
-
this.sendToTokenizer(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})));
|
|
1857
|
+
this.sendToTokenizer(Object.assign(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})), { debug: this._debug }));
|
|
1779
1858
|
(_c = this._onReady) === null || _c === void 0 ? void 0 : _c.call(this);
|
|
1780
1859
|
this.log('tokenizer iframe ready', { protocolVersion: (_d = msg.__ozVersion) !== null && _d !== void 0 ? _d : null });
|
|
1860
|
+
this.log('vault state', this.debugState());
|
|
1781
1861
|
break;
|
|
1782
1862
|
case 'OZ_TOKEN_RESULT': {
|
|
1783
1863
|
if (typeof msg.requestId !== 'string' || !msg.requestId) {
|
|
@@ -1803,6 +1883,7 @@
|
|
|
1803
1883
|
pending.resolve(Object.assign(Object.assign({ token,
|
|
1804
1884
|
cvcSession }, (card ? { card } : {})), (pending.billing ? { billing: pending.billing } : {})));
|
|
1805
1885
|
this.log('token received', {
|
|
1886
|
+
requestIdPrefix: msg.requestId.slice(0, 12),
|
|
1806
1887
|
elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
|
|
1807
1888
|
tokenPresent: true,
|
|
1808
1889
|
cvcSessionPresent: true,
|
|
@@ -1834,7 +1915,7 @@
|
|
|
1834
1915
|
if (pending.timeoutId != null)
|
|
1835
1916
|
clearTimeout(pending.timeoutId);
|
|
1836
1917
|
const willRefresh = this.isRefreshableAuthError(errorCode, raw) && !pending.retried && Boolean(this._storedFetchWaxKey);
|
|
1837
|
-
this.log('token error', { errorCode, willRefresh });
|
|
1918
|
+
this.log('token error', { requestIdPrefix: msg.requestId.slice(0, 12), errorCode, willRefresh });
|
|
1838
1919
|
// Auto-refresh: if the wax key expired or was consumed and we haven't
|
|
1839
1920
|
// already retried for this request, transparently re-mint and retry.
|
|
1840
1921
|
if (willRefresh) {
|
|
@@ -1852,6 +1933,17 @@
|
|
|
1852
1933
|
pending.reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
1853
1934
|
return;
|
|
1854
1935
|
}
|
|
1936
|
+
// Verify all elements from the original call are still mounted and
|
|
1937
|
+
// ready. If any were destroyed (e.g. the merchant called
|
|
1938
|
+
// createElement() or destroy() during the async refresh), the
|
|
1939
|
+
// beginCollect() calls below would silently no-op — the tokenizer
|
|
1940
|
+
// would wait forever for field values that never arrive. Reject
|
|
1941
|
+
// immediately with a clear message instead of hanging 30 seconds.
|
|
1942
|
+
const allCardElementsStillReady = pending.readyElements.every(el => this.elements.has(el.frameId) && el.isReady);
|
|
1943
|
+
if (!allCardElementsStillReady) {
|
|
1944
|
+
pending.reject(new OzError('Card fields changed during session refresh. Please re-enter your card details.'));
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1855
1947
|
const newRequestId = `req-${uuid()}`;
|
|
1856
1948
|
// _tokenizing is still 'card' (cleanup() hasn't been called yet)
|
|
1857
1949
|
this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
|
|
@@ -1860,14 +1952,16 @@
|
|
|
1860
1952
|
this.sendToTokenizer({
|
|
1861
1953
|
type: 'OZ_TOKENIZE',
|
|
1862
1954
|
requestId: newRequestId,
|
|
1863
|
-
waxKey: this.waxKey,
|
|
1864
1955
|
tokenizationSessionId: this.tokenizationSessionId,
|
|
1865
1956
|
pubKey: this.pubKey,
|
|
1866
1957
|
firstName: pending.firstName,
|
|
1867
1958
|
lastName: pending.lastName,
|
|
1868
1959
|
fieldCount: pending.fieldCount,
|
|
1869
1960
|
}, retryCardChannels.map(ch => ch.port1));
|
|
1870
|
-
pending.readyElements.forEach((el, i) =>
|
|
1961
|
+
pending.readyElements.forEach((el, i) => {
|
|
1962
|
+
this.log('OZ_BEGIN_COLLECT dispatched (retry)', { type: el.type, requestIdPrefix: `${newRequestId.slice(0, 12)}...` });
|
|
1963
|
+
el.beginCollect(newRequestId, retryCardChannels[i].port2);
|
|
1964
|
+
});
|
|
1871
1965
|
const retryCardTimeoutId = setTimeout(() => {
|
|
1872
1966
|
if (this.tokenizeResolvers.has(newRequestId)) {
|
|
1873
1967
|
this.tokenizeResolvers.delete(newRequestId);
|
|
@@ -1884,68 +1978,80 @@
|
|
|
1884
1978
|
pending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry tokenization failed to start'));
|
|
1885
1979
|
}
|
|
1886
1980
|
}).catch((refreshErr) => {
|
|
1887
|
-
const
|
|
1888
|
-
pending.reject(new OzError(
|
|
1981
|
+
const errMsg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
|
|
1982
|
+
pending.reject(new OzError(errMsg, undefined, 'auth'));
|
|
1889
1983
|
});
|
|
1890
1984
|
break;
|
|
1891
1985
|
}
|
|
1892
1986
|
pending.reject(new OzError(normalizeVaultError(raw), raw, errorCode));
|
|
1893
1987
|
}
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
this.
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
this.
|
|
1929
|
-
|
|
1930
|
-
bankPending.
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1988
|
+
else {
|
|
1989
|
+
// Also check bank resolvers — both card and bank errors use OZ_TOKEN_ERROR.
|
|
1990
|
+
// Use else-if rather than sequential checks so a UUID collision (however
|
|
1991
|
+
// improbable) can never trigger double-rejection of two unrelated resolvers.
|
|
1992
|
+
const bankPending = this.bankTokenizeResolvers.get(msg.requestId);
|
|
1993
|
+
if (bankPending) {
|
|
1994
|
+
this.bankTokenizeResolvers.delete(msg.requestId);
|
|
1995
|
+
if (bankPending.timeoutId != null)
|
|
1996
|
+
clearTimeout(bankPending.timeoutId);
|
|
1997
|
+
if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
|
|
1998
|
+
const resetCountAtRetry = this._resetCount;
|
|
1999
|
+
this.refreshWaxKey().then(() => {
|
|
2000
|
+
if (this._destroyed) {
|
|
2001
|
+
bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
if (this._resetCount !== resetCountAtRetry) {
|
|
2005
|
+
bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
// Same stale-element guard as the card retry path above.
|
|
2009
|
+
const allBankElementsStillReady = bankPending.readyElements.every(el => this.elements.has(el.frameId) && el.isReady);
|
|
2010
|
+
if (!allBankElementsStillReady) {
|
|
2011
|
+
bankPending.reject(new OzError('Bank fields changed during session refresh. Please re-enter your account details.'));
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
const newRequestId = `req-${uuid()}`;
|
|
2015
|
+
this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
|
|
2016
|
+
try {
|
|
2017
|
+
const retryBankChannels = bankPending.readyElements.map(() => new MessageChannel());
|
|
2018
|
+
this.sendToTokenizer({
|
|
2019
|
+
type: 'OZ_BANK_TOKENIZE',
|
|
2020
|
+
requestId: newRequestId,
|
|
2021
|
+
tokenizationSessionId: this.tokenizationSessionId,
|
|
2022
|
+
pubKey: this.pubKey,
|
|
2023
|
+
firstName: bankPending.firstName,
|
|
2024
|
+
lastName: bankPending.lastName,
|
|
2025
|
+
fieldCount: bankPending.fieldCount,
|
|
2026
|
+
}, retryBankChannels.map(ch => ch.port1));
|
|
2027
|
+
bankPending.readyElements.forEach((el, i) => {
|
|
2028
|
+
this.log('OZ_BEGIN_COLLECT dispatched (retry)', { type: el.type, requestIdPrefix: `${newRequestId.slice(0, 12)}...` });
|
|
2029
|
+
el.beginCollect(newRequestId, retryBankChannels[i].port2);
|
|
2030
|
+
});
|
|
2031
|
+
const retryBankTimeoutId = setTimeout(() => {
|
|
2032
|
+
if (this.bankTokenizeResolvers.has(newRequestId)) {
|
|
2033
|
+
this.bankTokenizeResolvers.delete(newRequestId);
|
|
2034
|
+
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId: newRequestId });
|
|
2035
|
+
bankPending.reject(new OzError('Bank tokenization timed out after wax key refresh.', undefined, 'timeout'));
|
|
2036
|
+
}
|
|
2037
|
+
}, 30000);
|
|
2038
|
+
const retryBankEntry = this.bankTokenizeResolvers.get(newRequestId);
|
|
2039
|
+
if (retryBankEntry)
|
|
2040
|
+
retryBankEntry.timeoutId = retryBankTimeoutId;
|
|
2041
|
+
}
|
|
2042
|
+
catch (setupErr) {
|
|
2043
|
+
this.bankTokenizeResolvers.delete(newRequestId);
|
|
2044
|
+
bankPending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry bank tokenization failed to start'));
|
|
2045
|
+
}
|
|
2046
|
+
}).catch((refreshErr) => {
|
|
2047
|
+
const errMsg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
|
|
2048
|
+
bankPending.reject(new OzError(errMsg, undefined, 'auth'));
|
|
2049
|
+
});
|
|
2050
|
+
break;
|
|
2051
|
+
}
|
|
2052
|
+
bankPending.reject(new OzError(normalizeBankVaultError(raw), raw, errorCode));
|
|
1946
2053
|
}
|
|
1947
|
-
|
|
1948
|
-
}
|
|
2054
|
+
} // end else (bank path)
|
|
1949
2055
|
break;
|
|
1950
2056
|
}
|
|
1951
2057
|
case 'OZ_BANK_TOKEN_RESULT': {
|
|
@@ -1981,6 +2087,23 @@
|
|
|
1981
2087
|
}
|
|
1982
2088
|
break;
|
|
1983
2089
|
}
|
|
2090
|
+
case 'OZ_DEBUG_LOG': {
|
|
2091
|
+
// Relay warnings/errors from the tokenizer iframe into the parent page's
|
|
2092
|
+
// DevTools console. The tokenizer runs in a cross-origin iframe whose
|
|
2093
|
+
// console context is invisible to most developers unless they manually
|
|
2094
|
+
// switch the DevTools frame selector.
|
|
2095
|
+
// Errors are always surfaced; warnings only in debug mode to avoid
|
|
2096
|
+
// polluting production consoles.
|
|
2097
|
+
const level = typeof msg.level === 'string' ? msg.level : 'warn';
|
|
2098
|
+
const message = typeof msg.message === 'string' ? msg.message : String((_e = msg.message) !== null && _e !== void 0 ? _e : '');
|
|
2099
|
+
if (level === 'error') {
|
|
2100
|
+
console.error(`[OzVault:tokenizer] ${message}`);
|
|
2101
|
+
}
|
|
2102
|
+
else if (this._debug) {
|
|
2103
|
+
console.warn(`[OzVault:tokenizer] ${message}`);
|
|
2104
|
+
}
|
|
2105
|
+
break;
|
|
2106
|
+
}
|
|
1984
2107
|
}
|
|
1985
2108
|
}
|
|
1986
2109
|
/**
|
|
@@ -2029,6 +2152,7 @@
|
|
|
2029
2152
|
}
|
|
2030
2153
|
const newSessionId = uuid();
|
|
2031
2154
|
(_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
2155
|
+
const refreshStartMs = Date.now();
|
|
2032
2156
|
this.log('wax key refresh started');
|
|
2033
2157
|
this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
|
|
2034
2158
|
.then(newWaxKey => {
|
|
@@ -2041,12 +2165,12 @@
|
|
|
2041
2165
|
this._tokenizeSuccessCount = 0;
|
|
2042
2166
|
}
|
|
2043
2167
|
if (!this._destroyed && this.tokenizerReady) {
|
|
2044
|
-
this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey });
|
|
2168
|
+
this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey, debug: this._debug });
|
|
2045
2169
|
}
|
|
2046
|
-
this.log('wax key refresh succeeded');
|
|
2170
|
+
this.log('wax key refresh succeeded', { durationMs: Date.now() - refreshStartMs });
|
|
2047
2171
|
})
|
|
2048
2172
|
.catch((err) => {
|
|
2049
|
-
this.log('wax key refresh failed', { error: err instanceof Error ? err.message : String(err) });
|
|
2173
|
+
this.log('wax key refresh failed', { error: err instanceof Error ? err.message : String(err), durationMs: Date.now() - refreshStartMs });
|
|
2050
2174
|
throw err;
|
|
2051
2175
|
})
|
|
2052
2176
|
.finally(() => {
|