@sparkvault/sdk 1.11.3 → 1.21.4
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 +15 -0
- package/dist/identity/api.d.ts +3 -7
- package/dist/identity/handlers/index.d.ts +1 -1
- package/dist/identity/handlers/sparklink-handler.d.ts +5 -20
- package/dist/identity/renderer.d.ts +13 -9
- package/dist/identity/types.d.ts +2 -9
- package/dist/sparkvault.cjs.js +145 -101
- package/dist/sparkvault.cjs.js.map +1 -1
- package/dist/sparkvault.esm.js +145 -101
- 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/README.md
CHANGED
|
@@ -681,6 +681,21 @@ const result: VerifyResult = await sparkvault.identity.pop(options);
|
|
|
681
681
|
// result.identityType: 'email' | 'phone'
|
|
682
682
|
```
|
|
683
683
|
|
|
684
|
+
## Content Security Policy (CSP)
|
|
685
|
+
|
|
686
|
+
If your site uses a Content Security Policy, you must allow the SparkVault domains:
|
|
687
|
+
|
|
688
|
+
```
|
|
689
|
+
connect-src https://api.sparkvault.com wss://ws.sparkvault.com;
|
|
690
|
+
script-src https://cdn.sparkvault.com;
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
| Directive | Domain | Reason |
|
|
694
|
+
|-----------|--------|--------|
|
|
695
|
+
| `connect-src` | `https://api.sparkvault.com` | API requests (identity verification, config) |
|
|
696
|
+
| `connect-src` | `wss://ws.sparkvault.com` | WebSocket for SparkLink push notifications |
|
|
697
|
+
| `script-src` | `https://cdn.sparkvault.com` | SDK script (if using CDN installation) |
|
|
698
|
+
|
|
684
699
|
## Best Practices
|
|
685
700
|
|
|
686
701
|
1. **Initialize Once** — Create the SparkVault instance once when your app loads, not on every login attempt. The SDK preloads configuration for instant modal opening.
|
package/dist/identity/api.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Single responsibility: API calls only.
|
|
6
6
|
*/
|
|
7
7
|
import type { ResolvedConfig } from '../config';
|
|
8
|
-
import type { SdkConfig, TotpSendResponse, TotpVerifyResponse, PasskeyChallengeResponse, PasskeyVerifyResponse, SparkLinkSendResponse,
|
|
8
|
+
import type { SdkConfig, TotpSendResponse, TotpVerifyResponse, PasskeyChallengeResponse, PasskeyVerifyResponse, SparkLinkSendResponse, AuthContext } from './types';
|
|
9
9
|
export declare class IdentityApi {
|
|
10
10
|
private readonly config;
|
|
11
11
|
private readonly timeoutMs;
|
|
@@ -102,13 +102,9 @@ export declare class IdentityApi {
|
|
|
102
102
|
getSocialAuthUrl(provider: string, redirectUri: string, state: string): string;
|
|
103
103
|
/**
|
|
104
104
|
* Send SparkLink email for identity verification.
|
|
105
|
-
* Includes
|
|
105
|
+
* Includes connectionId for WebSocket push notification of verification result.
|
|
106
106
|
*/
|
|
107
|
-
sendSparkLink(email: string, authContext?: AuthContext): Promise<SparkLinkSendResponse>;
|
|
108
|
-
/**
|
|
109
|
-
* Check SparkLink verification status (polling endpoint)
|
|
110
|
-
*/
|
|
111
|
-
checkSparkLinkStatus(sparkId: string, _authContext?: AuthContext): Promise<SparkLinkStatusResponse>;
|
|
107
|
+
sendSparkLink(email: string, connectionId: string, authContext?: AuthContext): Promise<SparkLinkSendResponse>;
|
|
112
108
|
}
|
|
113
109
|
export declare class IdentityApiError extends Error {
|
|
114
110
|
readonly code: string;
|
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { PasskeyHandler, type PasskeyResult, type PasskeyCheckResult } from './passkey-handler';
|
|
8
8
|
export { TotpHandler, type TotpSendResult, type TotpVerifyResult } from './totp-handler';
|
|
9
|
-
export { SparkLinkHandler, type SparkLinkSendResult
|
|
9
|
+
export { SparkLinkHandler, type SparkLinkSendResult } from './sparklink-handler';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SparkLink Handler
|
|
3
3
|
*
|
|
4
|
-
* Single responsibility: SparkLink sending
|
|
5
|
-
*
|
|
4
|
+
* Single responsibility: SparkLink sending.
|
|
5
|
+
* Verification is handled via WebSocket push (no polling, no iframe).
|
|
6
6
|
*/
|
|
7
7
|
import type { IdentityApi } from '../api';
|
|
8
8
|
import type { VerificationState } from '../state';
|
|
@@ -16,18 +16,7 @@ export interface SparkLinkSendResult {
|
|
|
16
16
|
error?: string;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*/
|
|
21
|
-
export interface SparkLinkStatusResult {
|
|
22
|
-
verified: boolean;
|
|
23
|
-
token?: string;
|
|
24
|
-
identity?: string;
|
|
25
|
-
identityType?: string;
|
|
26
|
-
/** Redirect URL for OIDC/simple mode flows */
|
|
27
|
-
redirect?: string;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Handles SparkLink sending and verification polling
|
|
19
|
+
* Handles SparkLink sending
|
|
31
20
|
*/
|
|
32
21
|
export declare class SparkLinkHandler {
|
|
33
22
|
private readonly api;
|
|
@@ -35,11 +24,7 @@ export declare class SparkLinkHandler {
|
|
|
35
24
|
constructor(api: IdentityApi, state: VerificationState);
|
|
36
25
|
/**
|
|
37
26
|
* Send SparkLink to the user's email
|
|
27
|
+
* @param connectionId - WebSocket connectionId for push notification
|
|
38
28
|
*/
|
|
39
|
-
send(): Promise<SparkLinkSendResult>;
|
|
40
|
-
/**
|
|
41
|
-
* Check SparkLink verification status
|
|
42
|
-
* Called periodically by the renderer to poll for completion
|
|
43
|
-
*/
|
|
44
|
-
checkStatus(sparkId: string): Promise<SparkLinkStatusResult>;
|
|
29
|
+
send(connectionId: string): Promise<SparkLinkSendResult>;
|
|
45
30
|
}
|
|
@@ -23,8 +23,7 @@ export declare class IdentityRenderer {
|
|
|
23
23
|
private readonly sparkLinkHandler;
|
|
24
24
|
private currentView;
|
|
25
25
|
private focusTimeoutId;
|
|
26
|
-
private
|
|
27
|
-
private messageListener;
|
|
26
|
+
private sparkLinkWs;
|
|
28
27
|
constructor(container: Container, api: IdentityApi, options: VerifyOptions, callbacks: RendererCallbacks);
|
|
29
28
|
private get recipient();
|
|
30
29
|
private get identityType();
|
|
@@ -88,19 +87,24 @@ export declare class IdentityRenderer {
|
|
|
88
87
|
*/
|
|
89
88
|
private handlePasskeyPromptSkip;
|
|
90
89
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
90
|
+
* Open a WebSocket connection and return the connectionId.
|
|
91
|
+
* The server echoes back the connectionId on init; this is used to route
|
|
92
|
+
* the SparkLink verification result back to this SDK instance.
|
|
94
93
|
*/
|
|
95
|
-
private
|
|
94
|
+
private openSparkLinkWebSocket;
|
|
96
95
|
/**
|
|
97
|
-
*
|
|
96
|
+
* Set up WebSocket message handler for SparkLink verification results.
|
|
97
|
+
* Handles: sparklink_verified, sparklink_failed, sparklink_resent.
|
|
98
|
+
*/
|
|
99
|
+
private setupSparkLinkWsHandler;
|
|
100
|
+
/**
|
|
101
|
+
* Handle successful SparkLink verification via WebSocket push.
|
|
98
102
|
*/
|
|
99
103
|
private handleSparkLinkVerified;
|
|
100
104
|
/**
|
|
101
|
-
*
|
|
105
|
+
* Clean up SparkLink WebSocket connection.
|
|
102
106
|
*/
|
|
103
|
-
private
|
|
107
|
+
private cleanupSparkLink;
|
|
104
108
|
/**
|
|
105
109
|
* Handle resending SparkLink.
|
|
106
110
|
*/
|
package/dist/identity/types.d.ts
CHANGED
|
@@ -113,6 +113,8 @@ export interface SdkConfig {
|
|
|
113
113
|
allowedIdentityTypes?: ('email' | 'phone')[];
|
|
114
114
|
/** Enabled method IDs */
|
|
115
115
|
methods: MethodId[];
|
|
116
|
+
/** WebSocket domain for SparkLink push notifications (e.g., 'ws.sparkvault.com') */
|
|
117
|
+
wsDomain?: string;
|
|
116
118
|
}
|
|
117
119
|
/** Method metadata for SDK rendering (static lookup) */
|
|
118
120
|
export interface MethodMetadata {
|
|
@@ -173,14 +175,6 @@ export interface SparkLinkSendResponse {
|
|
|
173
175
|
sparkId: string;
|
|
174
176
|
expiresAt: number;
|
|
175
177
|
}
|
|
176
|
-
export interface SparkLinkStatusResponse {
|
|
177
|
-
verified: boolean;
|
|
178
|
-
token?: string;
|
|
179
|
-
identity?: string;
|
|
180
|
-
identityType?: string;
|
|
181
|
-
/** Redirect URL for OIDC/simple mode flows */
|
|
182
|
-
redirect?: string;
|
|
183
|
-
}
|
|
184
178
|
export type IdentityViewState = {
|
|
185
179
|
view: 'loading';
|
|
186
180
|
} | {
|
|
@@ -210,7 +204,6 @@ export type IdentityViewState = {
|
|
|
210
204
|
} | {
|
|
211
205
|
view: 'sparklink-waiting';
|
|
212
206
|
email: string;
|
|
213
|
-
sparkId: string;
|
|
214
207
|
expiresAt: number;
|
|
215
208
|
} | {
|
|
216
209
|
view: 'oauth-pending';
|
package/dist/sparkvault.cjs.js
CHANGED
|
@@ -833,26 +833,16 @@ class IdentityApi {
|
|
|
833
833
|
}
|
|
834
834
|
/**
|
|
835
835
|
* Send SparkLink email for identity verification.
|
|
836
|
-
* Includes
|
|
836
|
+
* Includes connectionId for WebSocket push notification of verification result.
|
|
837
837
|
*/
|
|
838
|
-
async sendSparkLink(email, authContext) {
|
|
838
|
+
async sendSparkLink(email, connectionId, authContext) {
|
|
839
839
|
return this.request('POST', '/sparklink/send', {
|
|
840
840
|
email,
|
|
841
841
|
type: 'verify_identity',
|
|
842
|
-
|
|
843
|
-
// This allows the ceremony page to notify the SDK directly instead of polling
|
|
844
|
-
openerOrigin: typeof window !== 'undefined' ? window.location.origin : undefined,
|
|
842
|
+
connection_id: connectionId,
|
|
845
843
|
...this.buildAuthContextParams(authContext),
|
|
846
844
|
});
|
|
847
845
|
}
|
|
848
|
-
/**
|
|
849
|
-
* Check SparkLink verification status (polling endpoint)
|
|
850
|
-
*/
|
|
851
|
-
async checkSparkLinkStatus(sparkId, _authContext) {
|
|
852
|
-
// For GET requests with auth context, we'd need query params, but the status endpoint
|
|
853
|
-
// doesn't need auth context - it's already stored in the sparklink record
|
|
854
|
-
return this.request('GET', `/sparklink/status/${sparkId}`);
|
|
855
|
-
}
|
|
856
846
|
}
|
|
857
847
|
class IdentityApiError extends Error {
|
|
858
848
|
constructor(message, code, statusCode) {
|
|
@@ -1494,7 +1484,7 @@ class TotpHandler {
|
|
|
1494
1484
|
success: false,
|
|
1495
1485
|
newKindling: response.kindling,
|
|
1496
1486
|
retryAfter: response.retry_after,
|
|
1497
|
-
backoffExpires: response.
|
|
1487
|
+
backoffExpires: response.retry_after, // When backoff ends (not code expiry)
|
|
1498
1488
|
error: 'Invalid code. Please try again.',
|
|
1499
1489
|
};
|
|
1500
1490
|
}
|
|
@@ -1511,11 +1501,11 @@ class TotpHandler {
|
|
|
1511
1501
|
/**
|
|
1512
1502
|
* SparkLink Handler
|
|
1513
1503
|
*
|
|
1514
|
-
* Single responsibility: SparkLink sending
|
|
1515
|
-
*
|
|
1504
|
+
* Single responsibility: SparkLink sending.
|
|
1505
|
+
* Verification is handled via WebSocket push (no polling, no iframe).
|
|
1516
1506
|
*/
|
|
1517
1507
|
/**
|
|
1518
|
-
* Handles SparkLink sending
|
|
1508
|
+
* Handles SparkLink sending
|
|
1519
1509
|
*/
|
|
1520
1510
|
class SparkLinkHandler {
|
|
1521
1511
|
constructor(api, state) {
|
|
@@ -1524,10 +1514,11 @@ class SparkLinkHandler {
|
|
|
1524
1514
|
}
|
|
1525
1515
|
/**
|
|
1526
1516
|
* Send SparkLink to the user's email
|
|
1517
|
+
* @param connectionId - WebSocket connectionId for push notification
|
|
1527
1518
|
*/
|
|
1528
|
-
async send() {
|
|
1519
|
+
async send(connectionId) {
|
|
1529
1520
|
try {
|
|
1530
|
-
const result = await this.api.sendSparkLink(this.state.recipient, this.state.authContext);
|
|
1521
|
+
const result = await this.api.sendSparkLink(this.state.recipient, connectionId, this.state.authContext);
|
|
1531
1522
|
return {
|
|
1532
1523
|
success: true,
|
|
1533
1524
|
sparkId: result.sparkId,
|
|
@@ -1542,26 +1533,6 @@ class SparkLinkHandler {
|
|
|
1542
1533
|
};
|
|
1543
1534
|
}
|
|
1544
1535
|
}
|
|
1545
|
-
/**
|
|
1546
|
-
* Check SparkLink verification status
|
|
1547
|
-
* Called periodically by the renderer to poll for completion
|
|
1548
|
-
*/
|
|
1549
|
-
async checkStatus(sparkId) {
|
|
1550
|
-
try {
|
|
1551
|
-
const result = await this.api.checkSparkLinkStatus(sparkId);
|
|
1552
|
-
return {
|
|
1553
|
-
verified: result.verified,
|
|
1554
|
-
token: result.token,
|
|
1555
|
-
identity: result.identity,
|
|
1556
|
-
identityType: result.identityType,
|
|
1557
|
-
redirect: result.redirect,
|
|
1558
|
-
};
|
|
1559
|
-
}
|
|
1560
|
-
catch {
|
|
1561
|
-
// On error, return not verified (polling will continue)
|
|
1562
|
-
return { verified: false };
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
1536
|
}
|
|
1566
1537
|
|
|
1567
1538
|
/**
|
|
@@ -4234,7 +4205,7 @@ class SparkLinkWaitingView {
|
|
|
4234
4205
|
* Switch to expired state - static icon, no animation, helpful message
|
|
4235
4206
|
*/
|
|
4236
4207
|
showExpiredState() {
|
|
4237
|
-
// Notify renderer to
|
|
4208
|
+
// Notify renderer to clean up WebSocket connection
|
|
4238
4209
|
this.props.onExpired();
|
|
4239
4210
|
// Update waiting section: replace spinner with expired icon
|
|
4240
4211
|
if (this.waitingSection) {
|
|
@@ -4381,9 +4352,8 @@ class IdentityRenderer {
|
|
|
4381
4352
|
// View management
|
|
4382
4353
|
this.currentView = null;
|
|
4383
4354
|
this.focusTimeoutId = null;
|
|
4384
|
-
// SparkLink
|
|
4385
|
-
this.
|
|
4386
|
-
this.messageListener = null;
|
|
4355
|
+
// SparkLink WebSocket
|
|
4356
|
+
this.sparkLinkWs = null;
|
|
4387
4357
|
this.container = container;
|
|
4388
4358
|
this.api = api;
|
|
4389
4359
|
this.options = options;
|
|
@@ -4482,7 +4452,7 @@ class IdentityRenderer {
|
|
|
4482
4452
|
clearTimeout(this.focusTimeoutId);
|
|
4483
4453
|
this.focusTimeoutId = null;
|
|
4484
4454
|
}
|
|
4485
|
-
this.
|
|
4455
|
+
this.cleanupSparkLink();
|
|
4486
4456
|
this.destroyCurrentView();
|
|
4487
4457
|
this.container.destroy();
|
|
4488
4458
|
}
|
|
@@ -4587,7 +4557,7 @@ class IdentityRenderer {
|
|
|
4587
4557
|
onResend: () => this.handleSparkLinkResend(),
|
|
4588
4558
|
onFallback: () => this.handleSparkLinkFallback(),
|
|
4589
4559
|
onBack: () => this.showMethodSelect(),
|
|
4590
|
-
onExpired: () => this.
|
|
4560
|
+
onExpired: () => this.cleanupSparkLink(),
|
|
4591
4561
|
});
|
|
4592
4562
|
case 'oauth-pending':
|
|
4593
4563
|
return new LoadingView({ message: `Connecting to ${state.provider}...` });
|
|
@@ -4717,8 +4687,20 @@ class IdentityRenderer {
|
|
|
4717
4687
|
}
|
|
4718
4688
|
case 'sparklink': {
|
|
4719
4689
|
this.setState({ view: 'loading' });
|
|
4720
|
-
|
|
4690
|
+
// Open WebSocket and get connectionId before sending
|
|
4691
|
+
const connectionId = await this.openSparkLinkWebSocket();
|
|
4692
|
+
if (!connectionId) {
|
|
4693
|
+
this.cleanupSparkLink();
|
|
4694
|
+
this.setState({
|
|
4695
|
+
view: 'error',
|
|
4696
|
+
message: 'Failed to establish connection',
|
|
4697
|
+
code: 'sparklink_ws_failed',
|
|
4698
|
+
});
|
|
4699
|
+
return;
|
|
4700
|
+
}
|
|
4701
|
+
const result = await this.sparkLinkHandler.send(connectionId);
|
|
4721
4702
|
if (!result.success) {
|
|
4703
|
+
this.cleanupSparkLink();
|
|
4722
4704
|
this.setState({
|
|
4723
4705
|
view: 'error',
|
|
4724
4706
|
message: result.error ?? 'Failed to send SparkLink',
|
|
@@ -4729,10 +4711,8 @@ class IdentityRenderer {
|
|
|
4729
4711
|
this.setState({
|
|
4730
4712
|
view: 'sparklink-waiting',
|
|
4731
4713
|
email: this.recipient,
|
|
4732
|
-
sparkId: result.sparkId,
|
|
4733
4714
|
expiresAt: result.expiresAt,
|
|
4734
4715
|
});
|
|
4735
|
-
this.startSparkLinkPolling(result.sparkId);
|
|
4736
4716
|
break;
|
|
4737
4717
|
}
|
|
4738
4718
|
case 'social': {
|
|
@@ -5018,51 +4998,110 @@ class IdentityRenderer {
|
|
|
5018
4998
|
this.callbacks.onSuccess(pendingResult);
|
|
5019
4999
|
}
|
|
5020
5000
|
/**
|
|
5021
|
-
*
|
|
5022
|
-
*
|
|
5023
|
-
*
|
|
5001
|
+
* Open a WebSocket connection and return the connectionId.
|
|
5002
|
+
* The server echoes back the connectionId on init; this is used to route
|
|
5003
|
+
* the SparkLink verification result back to this SDK instance.
|
|
5024
5004
|
*/
|
|
5025
|
-
|
|
5026
|
-
this.
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5005
|
+
openSparkLinkWebSocket() {
|
|
5006
|
+
this.cleanupSparkLink();
|
|
5007
|
+
const wsDomain = this.sdkConfig?.wsDomain || 'ws.sparkvault.com';
|
|
5008
|
+
const wsUrl = `wss://${wsDomain}`;
|
|
5009
|
+
return new Promise((resolve) => {
|
|
5010
|
+
let resolved = false;
|
|
5011
|
+
const ws = new WebSocket(wsUrl);
|
|
5012
|
+
this.sparkLinkWs = ws;
|
|
5013
|
+
const timeout = setTimeout(() => {
|
|
5014
|
+
if (!resolved) {
|
|
5015
|
+
resolved = true;
|
|
5016
|
+
ws.close();
|
|
5017
|
+
resolve(null);
|
|
5018
|
+
}
|
|
5019
|
+
}, 10000);
|
|
5020
|
+
ws.onopen = () => {
|
|
5021
|
+
ws.send(JSON.stringify({ action: 'init' }));
|
|
5022
|
+
};
|
|
5023
|
+
ws.onmessage = (event) => {
|
|
5024
|
+
let data;
|
|
5025
|
+
try {
|
|
5026
|
+
data = JSON.parse(event.data);
|
|
5027
|
+
}
|
|
5028
|
+
catch {
|
|
5029
|
+
return;
|
|
5030
|
+
}
|
|
5031
|
+
if (!resolved && data.type === 'connection' && typeof data.connectionId === 'string') {
|
|
5032
|
+
resolved = true;
|
|
5033
|
+
clearTimeout(timeout);
|
|
5034
|
+
this.setupSparkLinkWsHandler(ws);
|
|
5035
|
+
resolve(data.connectionId);
|
|
5036
|
+
return;
|
|
5037
|
+
}
|
|
5038
|
+
};
|
|
5039
|
+
ws.onerror = () => {
|
|
5040
|
+
if (!resolved) {
|
|
5041
|
+
resolved = true;
|
|
5042
|
+
clearTimeout(timeout);
|
|
5043
|
+
resolve(null);
|
|
5044
|
+
}
|
|
5045
|
+
};
|
|
5046
|
+
ws.onclose = () => {
|
|
5047
|
+
if (!resolved) {
|
|
5048
|
+
resolved = true;
|
|
5049
|
+
clearTimeout(timeout);
|
|
5050
|
+
resolve(null);
|
|
5051
|
+
}
|
|
5052
|
+
};
|
|
5053
|
+
});
|
|
5054
|
+
}
|
|
5055
|
+
/**
|
|
5056
|
+
* Set up WebSocket message handler for SparkLink verification results.
|
|
5057
|
+
* Handles: sparklink_verified, sparklink_failed, sparklink_resent.
|
|
5058
|
+
*/
|
|
5059
|
+
setupSparkLinkWsHandler(ws) {
|
|
5060
|
+
ws.onmessage = (event) => {
|
|
5061
|
+
let data;
|
|
5062
|
+
try {
|
|
5063
|
+
data = JSON.parse(event.data);
|
|
5064
|
+
}
|
|
5065
|
+
catch {
|
|
5038
5066
|
return;
|
|
5039
|
-
this.handleSparkLinkVerified({
|
|
5040
|
-
token,
|
|
5041
|
-
identity,
|
|
5042
|
-
identityType: identityType || 'email',
|
|
5043
|
-
redirect,
|
|
5044
|
-
});
|
|
5045
|
-
};
|
|
5046
|
-
window.addEventListener('message', this.messageListener);
|
|
5047
|
-
// Fallback: Poll status endpoint every 2 seconds
|
|
5048
|
-
// This catches cases where postMessage might not work (popup blockers, etc)
|
|
5049
|
-
this.pollingInterval = setInterval(async () => {
|
|
5050
|
-
const status = await this.sparkLinkHandler.checkStatus(sparkId);
|
|
5051
|
-
if (status.verified && status.token && status.identity) {
|
|
5052
|
-
this.handleSparkLinkVerified({
|
|
5053
|
-
token: status.token,
|
|
5054
|
-
identity: status.identity,
|
|
5055
|
-
identityType: status.identityType || 'email',
|
|
5056
|
-
redirect: status.redirect,
|
|
5057
|
-
});
|
|
5058
5067
|
}
|
|
5059
|
-
|
|
5068
|
+
switch (data.type) {
|
|
5069
|
+
case 'sparklink_verified':
|
|
5070
|
+
this.handleSparkLinkVerified({
|
|
5071
|
+
token: data.token,
|
|
5072
|
+
identity: data.identity,
|
|
5073
|
+
identityType: data.identityType || 'email',
|
|
5074
|
+
});
|
|
5075
|
+
break;
|
|
5076
|
+
case 'sparklink_failed':
|
|
5077
|
+
this.cleanupSparkLink();
|
|
5078
|
+
this.setState({
|
|
5079
|
+
view: 'error',
|
|
5080
|
+
message: 'Verification failed. Please try again or choose a different method.',
|
|
5081
|
+
code: 'sparklink_failed',
|
|
5082
|
+
});
|
|
5083
|
+
break;
|
|
5084
|
+
case 'sparklink_resent':
|
|
5085
|
+
// Server auto-resent a new link — update countdown timer
|
|
5086
|
+
if (typeof data.expiresAt === 'number') {
|
|
5087
|
+
this.setState({
|
|
5088
|
+
view: 'sparklink-waiting',
|
|
5089
|
+
email: this.recipient,
|
|
5090
|
+
expiresAt: data.expiresAt,
|
|
5091
|
+
});
|
|
5092
|
+
}
|
|
5093
|
+
break;
|
|
5094
|
+
}
|
|
5095
|
+
};
|
|
5096
|
+
ws.onclose = () => {
|
|
5097
|
+
this.sparkLinkWs = null;
|
|
5098
|
+
};
|
|
5060
5099
|
}
|
|
5061
5100
|
/**
|
|
5062
|
-
* Handle successful SparkLink verification
|
|
5101
|
+
* Handle successful SparkLink verification via WebSocket push.
|
|
5063
5102
|
*/
|
|
5064
5103
|
async handleSparkLinkVerified(result) {
|
|
5065
|
-
this.
|
|
5104
|
+
this.cleanupSparkLink();
|
|
5066
5105
|
// Handle redirect for OIDC/simple mode flows
|
|
5067
5106
|
if (result.redirect) {
|
|
5068
5107
|
this.close();
|
|
@@ -5082,39 +5121,44 @@ class IdentityRenderer {
|
|
|
5082
5121
|
this.callbacks.onSuccess(result);
|
|
5083
5122
|
}
|
|
5084
5123
|
/**
|
|
5085
|
-
*
|
|
5124
|
+
* Clean up SparkLink WebSocket connection.
|
|
5086
5125
|
*/
|
|
5087
|
-
|
|
5088
|
-
if (this.
|
|
5089
|
-
|
|
5090
|
-
this.
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
this.messageListener = null;
|
|
5126
|
+
cleanupSparkLink() {
|
|
5127
|
+
if (this.sparkLinkWs) {
|
|
5128
|
+
this.sparkLinkWs.onmessage = null;
|
|
5129
|
+
this.sparkLinkWs.onclose = null;
|
|
5130
|
+
this.sparkLinkWs.onerror = null;
|
|
5131
|
+
this.sparkLinkWs.close();
|
|
5132
|
+
this.sparkLinkWs = null;
|
|
5095
5133
|
}
|
|
5096
5134
|
}
|
|
5097
5135
|
/**
|
|
5098
5136
|
* Handle resending SparkLink.
|
|
5099
5137
|
*/
|
|
5100
5138
|
async handleSparkLinkResend() {
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5139
|
+
// Open new WebSocket for the resend
|
|
5140
|
+
const connectionId = await this.openSparkLinkWebSocket();
|
|
5141
|
+
if (!connectionId) {
|
|
5142
|
+
this.cleanupSparkLink();
|
|
5143
|
+
return;
|
|
5144
|
+
}
|
|
5145
|
+
const result = await this.sparkLinkHandler.send(connectionId);
|
|
5146
|
+
if (result.success) {
|
|
5104
5147
|
this.setState({
|
|
5105
5148
|
view: 'sparklink-waiting',
|
|
5106
5149
|
email: this.recipient,
|
|
5107
|
-
sparkId: result.sparkId,
|
|
5108
5150
|
expiresAt: result.expiresAt,
|
|
5109
5151
|
});
|
|
5110
|
-
|
|
5152
|
+
}
|
|
5153
|
+
else {
|
|
5154
|
+
this.cleanupSparkLink();
|
|
5111
5155
|
}
|
|
5112
5156
|
}
|
|
5113
5157
|
/**
|
|
5114
5158
|
* Handle fallback from SparkLink to TOTP verification code.
|
|
5115
5159
|
*/
|
|
5116
5160
|
async handleSparkLinkFallback() {
|
|
5117
|
-
this.
|
|
5161
|
+
this.cleanupSparkLink();
|
|
5118
5162
|
this.setState({ view: 'loading' });
|
|
5119
5163
|
const result = await this.totpHandler.send('email');
|
|
5120
5164
|
if (!result.success) {
|