@sparkvault/sdk 1.11.2 → 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 +146 -101
- package/dist/sparkvault.cjs.js.map +1 -1
- package/dist/sparkvault.esm.js +146 -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
|
/**
|
|
@@ -1690,6 +1661,7 @@ function getStyles(options) {
|
|
|
1690
1661
|
padding: 14px 20px;
|
|
1691
1662
|
border-bottom: 1px solid ${tokens.border};
|
|
1692
1663
|
background: ${tokens.bg};
|
|
1664
|
+
border-radius: 16px 16px 0 0;
|
|
1693
1665
|
flex-shrink: 0;
|
|
1694
1666
|
}
|
|
1695
1667
|
|
|
@@ -4233,7 +4205,7 @@ class SparkLinkWaitingView {
|
|
|
4233
4205
|
* Switch to expired state - static icon, no animation, helpful message
|
|
4234
4206
|
*/
|
|
4235
4207
|
showExpiredState() {
|
|
4236
|
-
// Notify renderer to
|
|
4208
|
+
// Notify renderer to clean up WebSocket connection
|
|
4237
4209
|
this.props.onExpired();
|
|
4238
4210
|
// Update waiting section: replace spinner with expired icon
|
|
4239
4211
|
if (this.waitingSection) {
|
|
@@ -4380,9 +4352,8 @@ class IdentityRenderer {
|
|
|
4380
4352
|
// View management
|
|
4381
4353
|
this.currentView = null;
|
|
4382
4354
|
this.focusTimeoutId = null;
|
|
4383
|
-
// SparkLink
|
|
4384
|
-
this.
|
|
4385
|
-
this.messageListener = null;
|
|
4355
|
+
// SparkLink WebSocket
|
|
4356
|
+
this.sparkLinkWs = null;
|
|
4386
4357
|
this.container = container;
|
|
4387
4358
|
this.api = api;
|
|
4388
4359
|
this.options = options;
|
|
@@ -4481,7 +4452,7 @@ class IdentityRenderer {
|
|
|
4481
4452
|
clearTimeout(this.focusTimeoutId);
|
|
4482
4453
|
this.focusTimeoutId = null;
|
|
4483
4454
|
}
|
|
4484
|
-
this.
|
|
4455
|
+
this.cleanupSparkLink();
|
|
4485
4456
|
this.destroyCurrentView();
|
|
4486
4457
|
this.container.destroy();
|
|
4487
4458
|
}
|
|
@@ -4586,7 +4557,7 @@ class IdentityRenderer {
|
|
|
4586
4557
|
onResend: () => this.handleSparkLinkResend(),
|
|
4587
4558
|
onFallback: () => this.handleSparkLinkFallback(),
|
|
4588
4559
|
onBack: () => this.showMethodSelect(),
|
|
4589
|
-
onExpired: () => this.
|
|
4560
|
+
onExpired: () => this.cleanupSparkLink(),
|
|
4590
4561
|
});
|
|
4591
4562
|
case 'oauth-pending':
|
|
4592
4563
|
return new LoadingView({ message: `Connecting to ${state.provider}...` });
|
|
@@ -4716,8 +4687,20 @@ class IdentityRenderer {
|
|
|
4716
4687
|
}
|
|
4717
4688
|
case 'sparklink': {
|
|
4718
4689
|
this.setState({ view: 'loading' });
|
|
4719
|
-
|
|
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);
|
|
4720
4702
|
if (!result.success) {
|
|
4703
|
+
this.cleanupSparkLink();
|
|
4721
4704
|
this.setState({
|
|
4722
4705
|
view: 'error',
|
|
4723
4706
|
message: result.error ?? 'Failed to send SparkLink',
|
|
@@ -4728,10 +4711,8 @@ class IdentityRenderer {
|
|
|
4728
4711
|
this.setState({
|
|
4729
4712
|
view: 'sparklink-waiting',
|
|
4730
4713
|
email: this.recipient,
|
|
4731
|
-
sparkId: result.sparkId,
|
|
4732
4714
|
expiresAt: result.expiresAt,
|
|
4733
4715
|
});
|
|
4734
|
-
this.startSparkLinkPolling(result.sparkId);
|
|
4735
4716
|
break;
|
|
4736
4717
|
}
|
|
4737
4718
|
case 'social': {
|
|
@@ -5017,51 +4998,110 @@ class IdentityRenderer {
|
|
|
5017
4998
|
this.callbacks.onSuccess(pendingResult);
|
|
5018
4999
|
}
|
|
5019
5000
|
/**
|
|
5020
|
-
*
|
|
5021
|
-
*
|
|
5022
|
-
*
|
|
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.
|
|
5023
5004
|
*/
|
|
5024
|
-
|
|
5025
|
-
this.
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
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 {
|
|
5037
5066
|
return;
|
|
5038
|
-
this.handleSparkLinkVerified({
|
|
5039
|
-
token,
|
|
5040
|
-
identity,
|
|
5041
|
-
identityType: identityType || 'email',
|
|
5042
|
-
redirect,
|
|
5043
|
-
});
|
|
5044
|
-
};
|
|
5045
|
-
window.addEventListener('message', this.messageListener);
|
|
5046
|
-
// Fallback: Poll status endpoint every 2 seconds
|
|
5047
|
-
// This catches cases where postMessage might not work (popup blockers, etc)
|
|
5048
|
-
this.pollingInterval = setInterval(async () => {
|
|
5049
|
-
const status = await this.sparkLinkHandler.checkStatus(sparkId);
|
|
5050
|
-
if (status.verified && status.token && status.identity) {
|
|
5051
|
-
this.handleSparkLinkVerified({
|
|
5052
|
-
token: status.token,
|
|
5053
|
-
identity: status.identity,
|
|
5054
|
-
identityType: status.identityType || 'email',
|
|
5055
|
-
redirect: status.redirect,
|
|
5056
|
-
});
|
|
5057
5067
|
}
|
|
5058
|
-
|
|
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
|
+
};
|
|
5059
5099
|
}
|
|
5060
5100
|
/**
|
|
5061
|
-
* Handle successful SparkLink verification
|
|
5101
|
+
* Handle successful SparkLink verification via WebSocket push.
|
|
5062
5102
|
*/
|
|
5063
5103
|
async handleSparkLinkVerified(result) {
|
|
5064
|
-
this.
|
|
5104
|
+
this.cleanupSparkLink();
|
|
5065
5105
|
// Handle redirect for OIDC/simple mode flows
|
|
5066
5106
|
if (result.redirect) {
|
|
5067
5107
|
this.close();
|
|
@@ -5081,39 +5121,44 @@ class IdentityRenderer {
|
|
|
5081
5121
|
this.callbacks.onSuccess(result);
|
|
5082
5122
|
}
|
|
5083
5123
|
/**
|
|
5084
|
-
*
|
|
5124
|
+
* Clean up SparkLink WebSocket connection.
|
|
5085
5125
|
*/
|
|
5086
|
-
|
|
5087
|
-
if (this.
|
|
5088
|
-
|
|
5089
|
-
this.
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
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;
|
|
5094
5133
|
}
|
|
5095
5134
|
}
|
|
5096
5135
|
/**
|
|
5097
5136
|
* Handle resending SparkLink.
|
|
5098
5137
|
*/
|
|
5099
5138
|
async handleSparkLinkResend() {
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
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) {
|
|
5103
5147
|
this.setState({
|
|
5104
5148
|
view: 'sparklink-waiting',
|
|
5105
5149
|
email: this.recipient,
|
|
5106
|
-
sparkId: result.sparkId,
|
|
5107
5150
|
expiresAt: result.expiresAt,
|
|
5108
5151
|
});
|
|
5109
|
-
|
|
5152
|
+
}
|
|
5153
|
+
else {
|
|
5154
|
+
this.cleanupSparkLink();
|
|
5110
5155
|
}
|
|
5111
5156
|
}
|
|
5112
5157
|
/**
|
|
5113
5158
|
* Handle fallback from SparkLink to TOTP verification code.
|
|
5114
5159
|
*/
|
|
5115
5160
|
async handleSparkLinkFallback() {
|
|
5116
|
-
this.
|
|
5161
|
+
this.cleanupSparkLink();
|
|
5117
5162
|
this.setState({ view: 'loading' });
|
|
5118
5163
|
const result = await this.totpHandler.send('email');
|
|
5119
5164
|
if (!result.success) {
|