@sparkvault/sdk 1.21.6 → 1.21.8
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/dist/identity/api.d.ts +3 -0
- package/dist/identity/renderer.d.ts +6 -0
- package/dist/identity/utils.d.ts +13 -1
- package/dist/sparkvault.cjs.js +115 -9
- package/dist/sparkvault.cjs.js.map +1 -1
- package/dist/sparkvault.esm.js +115 -9
- package/dist/sparkvault.esm.js.map +1 -1
- package/dist/sparkvault.js +1 -1
- package/dist/sparkvault.js.map +1 -1
- package/package.json +1 -1
package/dist/sparkvault.esm.js
CHANGED
|
@@ -265,6 +265,7 @@ class HttpClient {
|
|
|
265
265
|
}
|
|
266
266
|
async parseResponse(response) {
|
|
267
267
|
// Handle empty responses (204 No Content, etc.)
|
|
268
|
+
// Callers expecting no body should use <void> as the generic type parameter.
|
|
268
269
|
const contentLength = response.headers.get('content-length');
|
|
269
270
|
if (response.status === 204 || contentLength === '0') {
|
|
270
271
|
return null;
|
|
@@ -539,6 +540,57 @@ function base64urlToString(base64url) {
|
|
|
539
540
|
* Consolidates duplicated logic from renderer, api, and index.
|
|
540
541
|
*/
|
|
541
542
|
// Re-export timer utilities
|
|
543
|
+
/**
|
|
544
|
+
* Common multi-part public suffixes where the last two labels form the TLD.
|
|
545
|
+
* Domains under these suffixes need three labels for a valid registrable domain
|
|
546
|
+
* (e.g., "app.client.co.uk" → registrable domain is "client.co.uk").
|
|
547
|
+
*/
|
|
548
|
+
const MULTI_PART_TLDS = new Set([
|
|
549
|
+
'co.uk', 'co.jp', 'co.kr', 'co.nz', 'co.za', 'co.in', 'co.id', 'co.th',
|
|
550
|
+
'com.au', 'com.br', 'com.cn', 'com.mx', 'com.sg', 'com.tw', 'com.hk',
|
|
551
|
+
'com.ar', 'com.co', 'com.my', 'com.ph', 'com.pk', 'com.tr', 'com.ua',
|
|
552
|
+
'com.vn', 'com.ng', 'com.eg', 'com.sa', 'com.pe', 'com.ec',
|
|
553
|
+
'net.au', 'net.br', 'net.cn', 'net.nz',
|
|
554
|
+
'org.au', 'org.br', 'org.cn', 'org.nz', 'org.uk',
|
|
555
|
+
'ac.uk', 'gov.uk', 'ac.jp', 'ne.jp', 'or.jp',
|
|
556
|
+
'ac.kr', 'go.kr', 'or.kr',
|
|
557
|
+
'ac.nz', 'govt.nz',
|
|
558
|
+
'ac.za', 'gov.za',
|
|
559
|
+
'ac.in', 'gov.in', 'net.in', 'org.in',
|
|
560
|
+
]);
|
|
561
|
+
/**
|
|
562
|
+
* Extract the root (registrable) domain from a hostname for cross-subdomain cookies.
|
|
563
|
+
* Returns the domain prefixed with a dot (e.g., ".client.com") or null when
|
|
564
|
+
* the domain attribute should not be set (localhost, IP addresses).
|
|
565
|
+
*
|
|
566
|
+
* @param hostname - The hostname to extract the root domain from
|
|
567
|
+
* @returns Root domain with leading dot, or null
|
|
568
|
+
*/
|
|
569
|
+
function getRootDomain(hostname) {
|
|
570
|
+
// Localhost — browsers reject domain attribute for localhost
|
|
571
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1')
|
|
572
|
+
return null;
|
|
573
|
+
// IPv4 addresses
|
|
574
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname))
|
|
575
|
+
return null;
|
|
576
|
+
// IPv6 addresses
|
|
577
|
+
if (hostname.includes(':'))
|
|
578
|
+
return null;
|
|
579
|
+
const parts = hostname.split('.');
|
|
580
|
+
// Single-label hostnames (no dots)
|
|
581
|
+
if (parts.length <= 1)
|
|
582
|
+
return null;
|
|
583
|
+
// Two-part domain (e.g., "client.com") — already the root
|
|
584
|
+
if (parts.length === 2)
|
|
585
|
+
return `.${hostname}`;
|
|
586
|
+
// Check for multi-part TLD (e.g., "co.uk" in "app.client.co.uk")
|
|
587
|
+
const lastTwo = parts.slice(-2).join('.');
|
|
588
|
+
if (MULTI_PART_TLDS.has(lastTwo) && parts.length >= 3) {
|
|
589
|
+
return `.${parts.slice(-3).join('.')}`;
|
|
590
|
+
}
|
|
591
|
+
// Standard TLD — root domain is last two parts (e.g., "app.client.com" → ".client.com")
|
|
592
|
+
return `.${lastTwo}`;
|
|
593
|
+
}
|
|
542
594
|
/**
|
|
543
595
|
* Get cookie value by name
|
|
544
596
|
* @param name - Cookie name
|
|
@@ -549,7 +601,10 @@ function getCookie(name) {
|
|
|
549
601
|
return match ? match[2] : null;
|
|
550
602
|
}
|
|
551
603
|
/**
|
|
552
|
-
* Set cookie with expiration
|
|
604
|
+
* Set cookie with expiration on the root domain for cross-subdomain access.
|
|
605
|
+
* The domain is automatically extracted from the current hostname and prefixed
|
|
606
|
+
* with a dot (e.g., ".client.com") so the cookie is shared across all subdomains.
|
|
607
|
+
*
|
|
553
608
|
* @param name - Cookie name
|
|
554
609
|
* @param value - Cookie value
|
|
555
610
|
* @param days - Days until expiration
|
|
@@ -558,7 +613,9 @@ function setCookie(name, value, days) {
|
|
|
558
613
|
const date = new Date();
|
|
559
614
|
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
|
560
615
|
const expires = `expires=${date.toUTCString()}`;
|
|
561
|
-
|
|
616
|
+
const domain = getRootDomain(window.location.hostname);
|
|
617
|
+
const domainAttr = domain ? `; domain=${domain}` : '';
|
|
618
|
+
document.cookie = `${name}=${value}; ${expires}; path=/${domainAttr}; SameSite=Lax`;
|
|
562
619
|
}
|
|
563
620
|
/**
|
|
564
621
|
* Generate cryptographically secure random state string
|
|
@@ -610,9 +667,11 @@ class IdentityApi {
|
|
|
610
667
|
}
|
|
611
668
|
const url = `${this.baseUrl}${endpoint}`;
|
|
612
669
|
const headers = {
|
|
613
|
-
'Content-Type': 'application/json',
|
|
614
670
|
'Accept': 'application/json',
|
|
615
671
|
};
|
|
672
|
+
if (body) {
|
|
673
|
+
headers['Content-Type'] = 'application/json';
|
|
674
|
+
}
|
|
616
675
|
// Create abort controller for request timeout
|
|
617
676
|
const timeoutController = new AbortController();
|
|
618
677
|
const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);
|
|
@@ -651,15 +710,28 @@ class IdentityApi {
|
|
|
651
710
|
return (json.data ?? json);
|
|
652
711
|
}
|
|
653
712
|
catch (error) {
|
|
654
|
-
// Convert AbortError to a more descriptive error
|
|
655
|
-
|
|
713
|
+
// Convert AbortError to a more descriptive error.
|
|
714
|
+
// Check error.name directly (DOMException may not extend Error in all environments).
|
|
715
|
+
if (error != null && typeof error === 'object' && 'name' in error && error.name === 'AbortError') {
|
|
656
716
|
// Check which signal triggered the abort
|
|
657
717
|
if (this.closeController.signal.aborted) {
|
|
658
718
|
throw new IdentityApiError('Request cancelled', 'cancelled', 0);
|
|
659
719
|
}
|
|
660
720
|
throw new IdentityApiError('Request timed out', 'timeout', 0);
|
|
661
721
|
}
|
|
662
|
-
|
|
722
|
+
// Convert TypeError to a user-friendly network error.
|
|
723
|
+
// WebKit (Safari/iOS) throws TypeError("Load failed"),
|
|
724
|
+
// Chrome/Firefox throw TypeError("Failed to fetch")
|
|
725
|
+
// when fetch() fails at the browser level (network error, CORS, DNS, etc.).
|
|
726
|
+
if (error instanceof TypeError) {
|
|
727
|
+
throw new IdentityApiError('Unable to connect. Please check your connection and try again.', 'network_error', 0);
|
|
728
|
+
}
|
|
729
|
+
// Re-throw IdentityApiError as-is (already has user-friendly message)
|
|
730
|
+
if (error instanceof IdentityApiError) {
|
|
731
|
+
throw error;
|
|
732
|
+
}
|
|
733
|
+
// Convert any other unexpected errors to a structured error
|
|
734
|
+
throw new IdentityApiError(error instanceof Error ? error.message : 'An unexpected error occurred', 'unknown_error', 0);
|
|
663
735
|
}
|
|
664
736
|
finally {
|
|
665
737
|
clearTimeout(timeoutId);
|
|
@@ -668,21 +740,34 @@ class IdentityApi {
|
|
|
668
740
|
/**
|
|
669
741
|
* Fetch SDK configuration (branding, enabled methods).
|
|
670
742
|
* Uses caching to avoid redundant requests - safe to call multiple times.
|
|
743
|
+
* If a cached request failed (e.g., transient network error during preload),
|
|
744
|
+
* clears the cache and retries to avoid permanently caching a rejected promise.
|
|
671
745
|
*/
|
|
672
746
|
async getConfig() {
|
|
673
747
|
if (!this.configCache) {
|
|
674
748
|
this.configCache = this.request('GET', '/config');
|
|
675
749
|
}
|
|
676
|
-
|
|
750
|
+
try {
|
|
751
|
+
return await this.configCache;
|
|
752
|
+
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
// Clear failed cache so the next call retries instead of returning
|
|
755
|
+
// the same rejected promise. This handles transient network failures
|
|
756
|
+
// (e.g., "Load failed" in Safari) that occurred during preloadConfig().
|
|
757
|
+
this.configCache = null;
|
|
758
|
+
throw error;
|
|
759
|
+
}
|
|
677
760
|
}
|
|
678
761
|
/**
|
|
679
762
|
* Preload the SDK configuration in the background.
|
|
680
763
|
* Called on SDK init when preloadConfig is enabled.
|
|
681
764
|
* The result is cached and used when verify() is called.
|
|
765
|
+
* If preload fails, getConfig() will clear the cache and retry.
|
|
682
766
|
*/
|
|
683
767
|
preloadConfig() {
|
|
684
768
|
if (!this.configCache) {
|
|
685
|
-
// Fire and forget - errors are handled when getConfig() is awaited
|
|
769
|
+
// Fire and forget - errors are handled when getConfig() is awaited.
|
|
770
|
+
// If this fails, getConfig() clears the cache and retries.
|
|
686
771
|
this.configCache = this.request('GET', '/config');
|
|
687
772
|
}
|
|
688
773
|
}
|
|
@@ -4411,7 +4496,7 @@ class IdentityRenderer {
|
|
|
4411
4496
|
if (!config) {
|
|
4412
4497
|
// Config not ready yet - show loading and await it
|
|
4413
4498
|
this.setState({ view: 'loading' });
|
|
4414
|
-
config = await this.
|
|
4499
|
+
config = await this.fetchConfigWithRetry();
|
|
4415
4500
|
}
|
|
4416
4501
|
this.verificationState.setConfig(config);
|
|
4417
4502
|
// Update modal header with branding if provided
|
|
@@ -4437,6 +4522,27 @@ class IdentityRenderer {
|
|
|
4437
4522
|
this.handleErrorWithRecovery(error, 'config_error');
|
|
4438
4523
|
}
|
|
4439
4524
|
}
|
|
4525
|
+
/**
|
|
4526
|
+
* Fetch config with a single retry on network errors.
|
|
4527
|
+
* Handles transient failures common during subdomain switches
|
|
4528
|
+
* (DNS resolution, connection setup, Safari ITP).
|
|
4529
|
+
*/
|
|
4530
|
+
async fetchConfigWithRetry() {
|
|
4531
|
+
try {
|
|
4532
|
+
return await this.api.getConfig();
|
|
4533
|
+
}
|
|
4534
|
+
catch (error) {
|
|
4535
|
+
// Only retry on network errors (not API errors like 404, 500, etc.)
|
|
4536
|
+
const isNetworkError = error instanceof IdentityApiError && error.code === 'network_error';
|
|
4537
|
+
if (!isNetworkError) {
|
|
4538
|
+
throw error;
|
|
4539
|
+
}
|
|
4540
|
+
// Wait briefly before retry (exponential backoff: 1s)
|
|
4541
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
4542
|
+
// Retry once - getConfig() will clear the failed cache and make a fresh request
|
|
4543
|
+
return this.api.getConfig();
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4440
4546
|
/**
|
|
4441
4547
|
* Close the modal and clean up.
|
|
4442
4548
|
* Cancels all pending API requests to prevent orphaned operations.
|