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