@sparkvault/sdk 1.0.0 → 1.1.6
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 +536 -502
- package/dist/identity/api.d.ts +4 -3
- package/dist/identity/renderer.d.ts +9 -2
- package/dist/identity/types.d.ts +2 -0
- package/dist/identity/views/icons.d.ts +4 -0
- package/dist/identity/views/sparklink-waiting.d.ts +7 -0
- package/dist/sparkvault.cjs.js +198 -49
- package/dist/sparkvault.cjs.js.map +1 -1
- package/dist/sparkvault.esm.js +198 -49
- 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
|
@@ -63,7 +63,7 @@ export declare class IdentityApi {
|
|
|
63
63
|
* Complete passkey registration
|
|
64
64
|
*/
|
|
65
65
|
completePasskeyRegister(params: {
|
|
66
|
-
|
|
66
|
+
session: Record<string, unknown>;
|
|
67
67
|
credential: PublicKeyCredential;
|
|
68
68
|
}): Promise<PasskeyVerifyResponse>;
|
|
69
69
|
/**
|
|
@@ -74,7 +74,7 @@ export declare class IdentityApi {
|
|
|
74
74
|
* Complete passkey verification
|
|
75
75
|
*/
|
|
76
76
|
completePasskeyVerify(params: {
|
|
77
|
-
|
|
77
|
+
session: Record<string, unknown>;
|
|
78
78
|
credential: PublicKeyCredential;
|
|
79
79
|
}): Promise<PasskeyVerifyResponse>;
|
|
80
80
|
/**
|
|
@@ -86,7 +86,8 @@ export declare class IdentityApi {
|
|
|
86
86
|
*/
|
|
87
87
|
getEnterpriseAuthUrl(provider: string, redirectUri: string, state: string): string;
|
|
88
88
|
/**
|
|
89
|
-
* Send SparkLink email for identity verification
|
|
89
|
+
* Send SparkLink email for identity verification.
|
|
90
|
+
* Includes openerOrigin for postMessage-based completion notification.
|
|
90
91
|
*/
|
|
91
92
|
sendSparkLink(email: string): Promise<SparkLinkSendResponse>;
|
|
92
93
|
/**
|
|
@@ -24,6 +24,7 @@ export declare class IdentityRenderer {
|
|
|
24
24
|
private currentView;
|
|
25
25
|
private focusTimeoutId;
|
|
26
26
|
private pollingInterval;
|
|
27
|
+
private messageListener;
|
|
27
28
|
constructor(container: Container, api: IdentityApi, options: VerifyOptions, callbacks: RendererCallbacks);
|
|
28
29
|
private get recipient();
|
|
29
30
|
private get identityType();
|
|
@@ -78,11 +79,17 @@ export declare class IdentityRenderer {
|
|
|
78
79
|
*/
|
|
79
80
|
private handlePasskeyPromptSkip;
|
|
80
81
|
/**
|
|
81
|
-
* Start
|
|
82
|
+
* Start listening for SparkLink verification completion.
|
|
83
|
+
* Uses postMessage as primary mechanism (instant notification from ceremony page),
|
|
84
|
+
* with polling as fallback for edge cases where postMessage might fail.
|
|
82
85
|
*/
|
|
83
86
|
private startSparkLinkPolling;
|
|
84
87
|
/**
|
|
85
|
-
*
|
|
88
|
+
* Handle successful SparkLink verification (from either postMessage or polling).
|
|
89
|
+
*/
|
|
90
|
+
private handleSparkLinkVerified;
|
|
91
|
+
/**
|
|
92
|
+
* Stop SparkLink polling and message listener.
|
|
86
93
|
*/
|
|
87
94
|
private stopSparkLinkPolling;
|
|
88
95
|
/**
|
package/dist/identity/types.d.ts
CHANGED
|
@@ -127,6 +127,8 @@ export interface PasskeyChallengeResponse {
|
|
|
127
127
|
type: 'public-key';
|
|
128
128
|
}>;
|
|
129
129
|
timeout: number;
|
|
130
|
+
/** Session object to pass back in complete request */
|
|
131
|
+
session: Record<string, unknown>;
|
|
130
132
|
}
|
|
131
133
|
export interface PasskeyVerifyResponse {
|
|
132
134
|
token: string;
|
|
@@ -28,6 +28,10 @@ export declare function createPasskeyIcon(): SVGSVGElement;
|
|
|
28
28
|
* Error icon - clean triangle alert
|
|
29
29
|
*/
|
|
30
30
|
export declare function createErrorIcon(): SVGSVGElement;
|
|
31
|
+
/**
|
|
32
|
+
* Expired icon - grayscale triangle alert for expired states
|
|
33
|
+
*/
|
|
34
|
+
export declare function createExpiredIcon(): SVGSVGElement;
|
|
31
35
|
/**
|
|
32
36
|
* Method icons
|
|
33
37
|
*/
|
|
@@ -11,12 +11,15 @@ export interface SparkLinkWaitingViewProps {
|
|
|
11
11
|
onResend: () => void;
|
|
12
12
|
onFallback: () => void;
|
|
13
13
|
onBack: () => void;
|
|
14
|
+
onExpired: () => void;
|
|
14
15
|
}
|
|
15
16
|
export declare class SparkLinkWaitingView implements View {
|
|
16
17
|
private readonly props;
|
|
17
18
|
private expirationTimer;
|
|
18
19
|
private resendTimer;
|
|
19
20
|
private countdownElement;
|
|
21
|
+
private countdownSection;
|
|
22
|
+
private waitingSection;
|
|
20
23
|
private resendButton;
|
|
21
24
|
private backLink;
|
|
22
25
|
private fallbackButton;
|
|
@@ -28,6 +31,10 @@ export declare class SparkLinkWaitingView implements View {
|
|
|
28
31
|
private createBackLink;
|
|
29
32
|
private formatTime;
|
|
30
33
|
private startExpirationTimer;
|
|
34
|
+
/**
|
|
35
|
+
* Switch to expired state - static icon, no animation, helpful message
|
|
36
|
+
*/
|
|
37
|
+
private showExpiredState;
|
|
31
38
|
private handleResend;
|
|
32
39
|
destroy(): void;
|
|
33
40
|
}
|
package/dist/sparkvault.cjs.js
CHANGED
|
@@ -730,7 +730,18 @@ class IdentityApi {
|
|
|
730
730
|
* Start passkey registration
|
|
731
731
|
*/
|
|
732
732
|
async startPasskeyRegister(email) {
|
|
733
|
-
|
|
733
|
+
// Backend returns { options: PublicKeyCredentialCreationOptions, session: {...} }
|
|
734
|
+
// Extract and flatten to match PasskeyChallengeResponse
|
|
735
|
+
const response = await this.request('POST', '/passkey/register', { email });
|
|
736
|
+
return {
|
|
737
|
+
challenge: response.options.challenge,
|
|
738
|
+
rpId: response.options.rp.id,
|
|
739
|
+
rpName: response.options.rp.name,
|
|
740
|
+
userId: response.options.user.id,
|
|
741
|
+
userName: response.options.user.name,
|
|
742
|
+
timeout: response.options.timeout,
|
|
743
|
+
session: response.session,
|
|
744
|
+
};
|
|
734
745
|
}
|
|
735
746
|
/**
|
|
736
747
|
* Complete passkey registration
|
|
@@ -738,7 +749,7 @@ class IdentityApi {
|
|
|
738
749
|
async completePasskeyRegister(params) {
|
|
739
750
|
const attestation = params.credential.response;
|
|
740
751
|
return this.request('POST', '/passkey/register/complete', {
|
|
741
|
-
|
|
752
|
+
session: params.session,
|
|
742
753
|
credential: {
|
|
743
754
|
id: params.credential.id,
|
|
744
755
|
rawId: arrayBufferToBase64url(params.credential.rawId),
|
|
@@ -754,7 +765,17 @@ class IdentityApi {
|
|
|
754
765
|
* Start passkey verification
|
|
755
766
|
*/
|
|
756
767
|
async startPasskeyVerify(email) {
|
|
757
|
-
|
|
768
|
+
// Backend returns { options: PublicKeyCredentialRequestOptions, session: {...} }
|
|
769
|
+
// Extract and flatten to match PasskeyChallengeResponse
|
|
770
|
+
const response = await this.request('POST', '/passkey/verify', { email });
|
|
771
|
+
return {
|
|
772
|
+
challenge: response.options.challenge,
|
|
773
|
+
rpId: response.options.rpId,
|
|
774
|
+
rpName: 'SparkVault Identity', // Not returned by verify endpoint
|
|
775
|
+
timeout: response.options.timeout,
|
|
776
|
+
allowCredentials: response.options.allowCredentials,
|
|
777
|
+
session: response.session,
|
|
778
|
+
};
|
|
758
779
|
}
|
|
759
780
|
/**
|
|
760
781
|
* Complete passkey verification
|
|
@@ -762,7 +783,7 @@ class IdentityApi {
|
|
|
762
783
|
async completePasskeyVerify(params) {
|
|
763
784
|
const assertion = params.credential.response;
|
|
764
785
|
return this.request('POST', '/passkey/verify/complete', {
|
|
765
|
-
|
|
786
|
+
session: params.session,
|
|
766
787
|
credential: {
|
|
767
788
|
id: params.credential.id,
|
|
768
789
|
rawId: arrayBufferToBase64url(params.credential.rawId),
|
|
@@ -799,12 +820,16 @@ class IdentityApi {
|
|
|
799
820
|
return `${this.baseUrl}/saml/${provider}?${params}`;
|
|
800
821
|
}
|
|
801
822
|
/**
|
|
802
|
-
* Send SparkLink email for identity verification
|
|
823
|
+
* Send SparkLink email for identity verification.
|
|
824
|
+
* Includes openerOrigin for postMessage-based completion notification.
|
|
803
825
|
*/
|
|
804
826
|
async sendSparkLink(email) {
|
|
805
827
|
return this.request('POST', '/sparklink/send', {
|
|
806
828
|
email,
|
|
807
829
|
type: 'verify_identity',
|
|
830
|
+
// Send opener origin for postMessage on verification completion
|
|
831
|
+
// This allows the ceremony page to notify the SDK directly instead of polling
|
|
832
|
+
openerOrigin: typeof window !== 'undefined' ? window.location.origin : undefined,
|
|
808
833
|
});
|
|
809
834
|
}
|
|
810
835
|
/**
|
|
@@ -1247,15 +1272,15 @@ class PasskeyHandler {
|
|
|
1247
1272
|
*/
|
|
1248
1273
|
async register() {
|
|
1249
1274
|
try {
|
|
1250
|
-
const
|
|
1275
|
+
const challengeResponse = await this.api.startPasskeyRegister(this.state.recipient);
|
|
1251
1276
|
const publicKeyOptions = {
|
|
1252
|
-
challenge: base64urlToArrayBuffer(
|
|
1277
|
+
challenge: base64urlToArrayBuffer(challengeResponse.challenge),
|
|
1253
1278
|
rp: {
|
|
1254
|
-
id:
|
|
1255
|
-
name:
|
|
1279
|
+
id: challengeResponse.rpId,
|
|
1280
|
+
name: challengeResponse.rpName,
|
|
1256
1281
|
},
|
|
1257
1282
|
user: {
|
|
1258
|
-
id: base64urlToArrayBuffer(
|
|
1283
|
+
id: base64urlToArrayBuffer(challengeResponse.userId ?? this.state.recipient),
|
|
1259
1284
|
name: this.state.recipient,
|
|
1260
1285
|
displayName: this.state.recipient,
|
|
1261
1286
|
},
|
|
@@ -1263,7 +1288,7 @@ class PasskeyHandler {
|
|
|
1263
1288
|
{ type: 'public-key', alg: -7 }, // ES256
|
|
1264
1289
|
{ type: 'public-key', alg: -257 }, // RS256
|
|
1265
1290
|
],
|
|
1266
|
-
timeout:
|
|
1291
|
+
timeout: challengeResponse.timeout,
|
|
1267
1292
|
authenticatorSelection: {
|
|
1268
1293
|
residentKey: 'preferred',
|
|
1269
1294
|
userVerification: 'preferred',
|
|
@@ -1281,7 +1306,7 @@ class PasskeyHandler {
|
|
|
1281
1306
|
};
|
|
1282
1307
|
}
|
|
1283
1308
|
const response = await this.api.completePasskeyRegister({
|
|
1284
|
-
|
|
1309
|
+
session: challengeResponse.session,
|
|
1285
1310
|
credential,
|
|
1286
1311
|
});
|
|
1287
1312
|
// Update state - user now has a passkey
|
|
@@ -1304,13 +1329,13 @@ class PasskeyHandler {
|
|
|
1304
1329
|
*/
|
|
1305
1330
|
async verify() {
|
|
1306
1331
|
try {
|
|
1307
|
-
const
|
|
1332
|
+
const challengeResponse = await this.api.startPasskeyVerify(this.state.recipient);
|
|
1308
1333
|
const publicKeyOptions = {
|
|
1309
|
-
challenge: base64urlToArrayBuffer(
|
|
1310
|
-
rpId:
|
|
1311
|
-
timeout:
|
|
1334
|
+
challenge: base64urlToArrayBuffer(challengeResponse.challenge),
|
|
1335
|
+
rpId: challengeResponse.rpId,
|
|
1336
|
+
timeout: challengeResponse.timeout,
|
|
1312
1337
|
userVerification: 'preferred',
|
|
1313
|
-
allowCredentials:
|
|
1338
|
+
allowCredentials: challengeResponse.allowCredentials?.map((cred) => ({
|
|
1314
1339
|
id: base64urlToArrayBuffer(cred.id),
|
|
1315
1340
|
type: cred.type,
|
|
1316
1341
|
transports: ['internal', 'hybrid', 'usb', 'ble', 'nfc'],
|
|
@@ -1327,7 +1352,7 @@ class PasskeyHandler {
|
|
|
1327
1352
|
};
|
|
1328
1353
|
}
|
|
1329
1354
|
const response = await this.api.completePasskeyVerify({
|
|
1330
|
-
|
|
1355
|
+
session: challengeResponse.session,
|
|
1331
1356
|
credential,
|
|
1332
1357
|
});
|
|
1333
1358
|
return {
|
|
@@ -1735,7 +1760,7 @@ function getStyles(options) {
|
|
|
1735
1760
|
======================================== */
|
|
1736
1761
|
|
|
1737
1762
|
.sv-body {
|
|
1738
|
-
padding:
|
|
1763
|
+
padding: 24px;
|
|
1739
1764
|
overflow-y: auto;
|
|
1740
1765
|
flex: 1;
|
|
1741
1766
|
}
|
|
@@ -2468,6 +2493,36 @@ function getStyles(options) {
|
|
|
2468
2493
|
color: ${tokens.textSecondary};
|
|
2469
2494
|
}
|
|
2470
2495
|
|
|
2496
|
+
/* SparkLink Expired State */
|
|
2497
|
+
.sv-sparklink-expired {
|
|
2498
|
+
flex-direction: column;
|
|
2499
|
+
gap: 12px;
|
|
2500
|
+
background: ${tokens.bgSubtle};
|
|
2501
|
+
border: 1px solid ${tokens.border};
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
.sv-sparklink-expired-icon {
|
|
2505
|
+
display: flex;
|
|
2506
|
+
justify-content: center;
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
.sv-sparklink-expired-icon svg {
|
|
2510
|
+
width: 48px;
|
|
2511
|
+
height: 48px;
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
.sv-sparklink-expired-text {
|
|
2515
|
+
font-size: 14px;
|
|
2516
|
+
font-weight: 500;
|
|
2517
|
+
color: ${tokens.textSecondary};
|
|
2518
|
+
text-align: center;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
.sv-expired-icon {
|
|
2522
|
+
width: 48px;
|
|
2523
|
+
height: 48px;
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2471
2526
|
/* ========================================
|
|
2472
2527
|
DIVIDER
|
|
2473
2528
|
======================================== */
|
|
@@ -2515,6 +2570,7 @@ function getStyles(options) {
|
|
|
2515
2570
|
.sv-inline-container {
|
|
2516
2571
|
display: flex;
|
|
2517
2572
|
flex-direction: column;
|
|
2573
|
+
height: 100%;
|
|
2518
2574
|
background: ${tokens.bg};
|
|
2519
2575
|
color: ${tokens.textPrimary};
|
|
2520
2576
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
@@ -2530,7 +2586,11 @@ function getStyles(options) {
|
|
|
2530
2586
|
}
|
|
2531
2587
|
|
|
2532
2588
|
.sv-inline-body {
|
|
2589
|
+
display: flex;
|
|
2590
|
+
flex-direction: column;
|
|
2591
|
+
justify-content: center;
|
|
2533
2592
|
min-height: 200px;
|
|
2593
|
+
flex: 1;
|
|
2534
2594
|
}
|
|
2535
2595
|
|
|
2536
2596
|
/* ========================================
|
|
@@ -2549,7 +2609,7 @@ function getStyles(options) {
|
|
|
2549
2609
|
}
|
|
2550
2610
|
|
|
2551
2611
|
.sv-body {
|
|
2552
|
-
padding:
|
|
2612
|
+
padding: 24px;
|
|
2553
2613
|
}
|
|
2554
2614
|
|
|
2555
2615
|
.sv-totp-digit {
|
|
@@ -2693,6 +2753,32 @@ function createErrorIcon() {
|
|
|
2693
2753
|
svg.appendChild(group);
|
|
2694
2754
|
return svg;
|
|
2695
2755
|
}
|
|
2756
|
+
/**
|
|
2757
|
+
* Expired icon - grayscale triangle alert for expired states
|
|
2758
|
+
*/
|
|
2759
|
+
function createExpiredIcon() {
|
|
2760
|
+
const svg = createSvgElement('0 0 48 48', 48, 48);
|
|
2761
|
+
svg.classList.add('sv-expired-icon');
|
|
2762
|
+
// Circle background - muted gray
|
|
2763
|
+
svg.appendChild(createCircle(24, 24, 22, {
|
|
2764
|
+
fill: '#F5F5F5',
|
|
2765
|
+
stroke: '#E5E5E5',
|
|
2766
|
+
'stroke-width': '1',
|
|
2767
|
+
}));
|
|
2768
|
+
// Alert triangle - gray
|
|
2769
|
+
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
2770
|
+
group.setAttribute('transform', 'translate(10, 12)');
|
|
2771
|
+
group.appendChild(createPath('M14 4L2 24h24L14 4z', { fill: '#737373' }));
|
|
2772
|
+
// Exclamation
|
|
2773
|
+
group.appendChild(createPath('M14 10v5', {
|
|
2774
|
+
stroke: '#FFFFFF',
|
|
2775
|
+
'stroke-width': '2',
|
|
2776
|
+
'stroke-linecap': 'round',
|
|
2777
|
+
}));
|
|
2778
|
+
group.appendChild(createCircle(14, 19, 1, { fill: '#FFFFFF' }));
|
|
2779
|
+
svg.appendChild(group);
|
|
2780
|
+
return svg;
|
|
2781
|
+
}
|
|
2696
2782
|
/**
|
|
2697
2783
|
* Method icons
|
|
2698
2784
|
*/
|
|
@@ -4025,6 +4111,8 @@ class SparkLinkWaitingView {
|
|
|
4025
4111
|
this.expirationTimer = null;
|
|
4026
4112
|
this.resendTimer = null;
|
|
4027
4113
|
this.countdownElement = null;
|
|
4114
|
+
this.countdownSection = null;
|
|
4115
|
+
this.waitingSection = null;
|
|
4028
4116
|
this.resendButton = null;
|
|
4029
4117
|
this.backLink = null;
|
|
4030
4118
|
this.fallbackButton = null;
|
|
@@ -4043,7 +4131,7 @@ class SparkLinkWaitingView {
|
|
|
4043
4131
|
subtitle.className = 'sv-subtitle';
|
|
4044
4132
|
subtitle.innerHTML = `We sent a secure link to<br><strong>${escapeHtml(this.props.email)}</strong>`;
|
|
4045
4133
|
// Spinner with message
|
|
4046
|
-
|
|
4134
|
+
this.waitingSection = div('sv-sparklink-waiting');
|
|
4047
4135
|
const spinner = document.createElement('div');
|
|
4048
4136
|
spinner.className = 'sv-spinner sv-spinner-small';
|
|
4049
4137
|
spinner.setAttribute('role', 'status');
|
|
@@ -4051,17 +4139,17 @@ class SparkLinkWaitingView {
|
|
|
4051
4139
|
const waitingText = document.createElement('span');
|
|
4052
4140
|
waitingText.className = 'sv-sparklink-waiting-text';
|
|
4053
4141
|
waitingText.textContent = 'Waiting for you to click the link...';
|
|
4054
|
-
waitingSection.appendChild(spinner);
|
|
4055
|
-
waitingSection.appendChild(waitingText);
|
|
4142
|
+
this.waitingSection.appendChild(spinner);
|
|
4143
|
+
this.waitingSection.appendChild(waitingText);
|
|
4056
4144
|
// Countdown timer
|
|
4057
|
-
|
|
4145
|
+
this.countdownSection = div('sv-sparklink-countdown');
|
|
4058
4146
|
const countdownLabel = document.createElement('span');
|
|
4059
4147
|
countdownLabel.textContent = 'Link expires in ';
|
|
4060
4148
|
this.countdownElement = document.createElement('span');
|
|
4061
4149
|
this.countdownElement.className = 'sv-sparklink-countdown-time';
|
|
4062
4150
|
this.startExpirationTimer();
|
|
4063
|
-
countdownSection.appendChild(countdownLabel);
|
|
4064
|
-
countdownSection.appendChild(this.countdownElement);
|
|
4151
|
+
this.countdownSection.appendChild(countdownLabel);
|
|
4152
|
+
this.countdownSection.appendChild(this.countdownElement);
|
|
4065
4153
|
// Resend button
|
|
4066
4154
|
const resendContainer = div('sv-resend-container');
|
|
4067
4155
|
this.resendButton = document.createElement('button');
|
|
@@ -4079,8 +4167,8 @@ class SparkLinkWaitingView {
|
|
|
4079
4167
|
container.appendChild(this.backLink);
|
|
4080
4168
|
container.appendChild(title);
|
|
4081
4169
|
container.appendChild(subtitle);
|
|
4082
|
-
container.appendChild(waitingSection);
|
|
4083
|
-
container.appendChild(countdownSection);
|
|
4170
|
+
container.appendChild(this.waitingSection);
|
|
4171
|
+
container.appendChild(this.countdownSection);
|
|
4084
4172
|
container.appendChild(resendContainer);
|
|
4085
4173
|
container.appendChild(fallbackContainer);
|
|
4086
4174
|
return container;
|
|
@@ -4122,13 +4210,36 @@ class SparkLinkWaitingView {
|
|
|
4122
4210
|
this.countdownElement.textContent = this.formatTime(secondsRemaining);
|
|
4123
4211
|
},
|
|
4124
4212
|
onExpired: () => {
|
|
4125
|
-
|
|
4126
|
-
return;
|
|
4127
|
-
this.countdownElement.textContent = 'Expired';
|
|
4213
|
+
this.showExpiredState();
|
|
4128
4214
|
},
|
|
4129
4215
|
});
|
|
4130
4216
|
this.expirationTimer.start();
|
|
4131
4217
|
}
|
|
4218
|
+
/**
|
|
4219
|
+
* Switch to expired state - static icon, no animation, helpful message
|
|
4220
|
+
*/
|
|
4221
|
+
showExpiredState() {
|
|
4222
|
+
// Notify renderer to stop polling
|
|
4223
|
+
this.props.onExpired();
|
|
4224
|
+
// Update waiting section: replace spinner with expired icon
|
|
4225
|
+
if (this.waitingSection) {
|
|
4226
|
+
this.waitingSection.innerHTML = '';
|
|
4227
|
+
this.waitingSection.classList.add('sv-sparklink-expired');
|
|
4228
|
+
// Add expired icon
|
|
4229
|
+
const iconContainer = div('sv-sparklink-expired-icon');
|
|
4230
|
+
iconContainer.appendChild(createExpiredIcon());
|
|
4231
|
+
this.waitingSection.appendChild(iconContainer);
|
|
4232
|
+
// Add expired message
|
|
4233
|
+
const expiredText = document.createElement('span');
|
|
4234
|
+
expiredText.className = 'sv-sparklink-expired-text';
|
|
4235
|
+
expiredText.textContent = 'This link has expired';
|
|
4236
|
+
this.waitingSection.appendChild(expiredText);
|
|
4237
|
+
}
|
|
4238
|
+
// Hide countdown section
|
|
4239
|
+
if (this.countdownSection) {
|
|
4240
|
+
this.countdownSection.style.display = 'none';
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4132
4243
|
handleResend() {
|
|
4133
4244
|
if (this.resendTimer?.isRunning() || !this.resendButton)
|
|
4134
4245
|
return;
|
|
@@ -4255,8 +4366,9 @@ class IdentityRenderer {
|
|
|
4255
4366
|
// View management
|
|
4256
4367
|
this.currentView = null;
|
|
4257
4368
|
this.focusTimeoutId = null;
|
|
4258
|
-
// SparkLink polling
|
|
4369
|
+
// SparkLink polling and postMessage listener
|
|
4259
4370
|
this.pollingInterval = null;
|
|
4371
|
+
this.messageListener = null;
|
|
4260
4372
|
this.container = container;
|
|
4261
4373
|
this.api = api;
|
|
4262
4374
|
this.options = options;
|
|
@@ -4378,7 +4490,11 @@ class IdentityRenderer {
|
|
|
4378
4490
|
this.currentView = view;
|
|
4379
4491
|
clearChildren(body);
|
|
4380
4492
|
body.appendChild(view.render());
|
|
4381
|
-
|
|
4493
|
+
// Auto-focus: prefer inputs first, then primary/method buttons (skip back links)
|
|
4494
|
+
const focusable = body.querySelector('input:not([disabled]), ' +
|
|
4495
|
+
'button.sv-btn-primary:not([disabled]), ' +
|
|
4496
|
+
'button.sv-btn-email-primary:not([disabled]), ' +
|
|
4497
|
+
'button.sv-btn-method:not([disabled])');
|
|
4382
4498
|
if (focusable) {
|
|
4383
4499
|
// Clear any previous focus timeout to avoid stacking
|
|
4384
4500
|
if (this.focusTimeoutId !== null) {
|
|
@@ -4451,6 +4567,7 @@ class IdentityRenderer {
|
|
|
4451
4567
|
onResend: () => this.handleSparkLinkResend(),
|
|
4452
4568
|
onFallback: () => this.handleSparkLinkFallback(),
|
|
4453
4569
|
onBack: () => this.showMethodSelect(),
|
|
4570
|
+
onExpired: () => this.stopSparkLinkPolling(),
|
|
4454
4571
|
});
|
|
4455
4572
|
case 'oauth-pending':
|
|
4456
4573
|
return new LoadingView({ message: `Connecting to ${state.provider}...` });
|
|
@@ -4793,41 +4910,73 @@ class IdentityRenderer {
|
|
|
4793
4910
|
this.callbacks.onSuccess(pendingResult);
|
|
4794
4911
|
}
|
|
4795
4912
|
/**
|
|
4796
|
-
* Start
|
|
4913
|
+
* Start listening for SparkLink verification completion.
|
|
4914
|
+
* Uses postMessage as primary mechanism (instant notification from ceremony page),
|
|
4915
|
+
* with polling as fallback for edge cases where postMessage might fail.
|
|
4797
4916
|
*/
|
|
4798
4917
|
startSparkLinkPolling(sparkId) {
|
|
4799
4918
|
this.stopSparkLinkPolling();
|
|
4919
|
+
// Primary: Listen for postMessage from ceremony page
|
|
4920
|
+
// This is faster and more reliable than polling
|
|
4921
|
+
this.messageListener = (event) => {
|
|
4922
|
+
// Validate message structure and type
|
|
4923
|
+
if (!event.data || typeof event.data !== 'object')
|
|
4924
|
+
return;
|
|
4925
|
+
if (event.data.type !== 'sparklink_verified')
|
|
4926
|
+
return;
|
|
4927
|
+
const { token, identity, identityType } = event.data;
|
|
4928
|
+
// Validate required fields
|
|
4929
|
+
if (!token || !identity)
|
|
4930
|
+
return;
|
|
4931
|
+
this.handleSparkLinkVerified({
|
|
4932
|
+
token,
|
|
4933
|
+
identity,
|
|
4934
|
+
identityType: identityType || 'email',
|
|
4935
|
+
});
|
|
4936
|
+
};
|
|
4937
|
+
window.addEventListener('message', this.messageListener);
|
|
4938
|
+
// Fallback: Poll status endpoint every 2 seconds
|
|
4939
|
+
// This catches cases where postMessage might not work (popup blockers, etc)
|
|
4800
4940
|
this.pollingInterval = setInterval(async () => {
|
|
4801
4941
|
const status = await this.sparkLinkHandler.checkStatus(sparkId);
|
|
4802
4942
|
if (status.verified && status.token && status.identity) {
|
|
4803
|
-
this.
|
|
4804
|
-
const result = {
|
|
4943
|
+
this.handleSparkLinkVerified({
|
|
4805
4944
|
token: status.token,
|
|
4806
4945
|
identity: status.identity,
|
|
4807
4946
|
identityType: status.identityType || 'email',
|
|
4808
|
-
};
|
|
4809
|
-
// Check if we should prompt for passkey registration
|
|
4810
|
-
if (await this.shouldShowPasskeyPrompt()) {
|
|
4811
|
-
this.setState({
|
|
4812
|
-
view: 'passkey-prompt',
|
|
4813
|
-
email: this.recipient,
|
|
4814
|
-
pendingResult: result,
|
|
4815
|
-
});
|
|
4816
|
-
return;
|
|
4817
|
-
}
|
|
4818
|
-
this.close();
|
|
4819
|
-
this.callbacks.onSuccess(result);
|
|
4947
|
+
});
|
|
4820
4948
|
}
|
|
4821
4949
|
}, 2000);
|
|
4822
4950
|
}
|
|
4823
4951
|
/**
|
|
4824
|
-
*
|
|
4952
|
+
* Handle successful SparkLink verification (from either postMessage or polling).
|
|
4953
|
+
*/
|
|
4954
|
+
async handleSparkLinkVerified(result) {
|
|
4955
|
+
this.stopSparkLinkPolling();
|
|
4956
|
+
// Check if we should prompt for passkey registration
|
|
4957
|
+
if (await this.shouldShowPasskeyPrompt()) {
|
|
4958
|
+
this.setState({
|
|
4959
|
+
view: 'passkey-prompt',
|
|
4960
|
+
email: this.recipient,
|
|
4961
|
+
pendingResult: result,
|
|
4962
|
+
});
|
|
4963
|
+
return;
|
|
4964
|
+
}
|
|
4965
|
+
this.close();
|
|
4966
|
+
this.callbacks.onSuccess(result);
|
|
4967
|
+
}
|
|
4968
|
+
/**
|
|
4969
|
+
* Stop SparkLink polling and message listener.
|
|
4825
4970
|
*/
|
|
4826
4971
|
stopSparkLinkPolling() {
|
|
4827
4972
|
if (this.pollingInterval) {
|
|
4828
4973
|
clearInterval(this.pollingInterval);
|
|
4829
4974
|
this.pollingInterval = null;
|
|
4830
4975
|
}
|
|
4976
|
+
if (this.messageListener) {
|
|
4977
|
+
window.removeEventListener('message', this.messageListener);
|
|
4978
|
+
this.messageListener = null;
|
|
4979
|
+
}
|
|
4831
4980
|
}
|
|
4832
4981
|
/**
|
|
4833
4982
|
* Handle resending SparkLink.
|