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