@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/dist/sparkvault.esm.js
CHANGED
|
@@ -829,26 +829,16 @@ class IdentityApi {
|
|
|
829
829
|
}
|
|
830
830
|
/**
|
|
831
831
|
* Send SparkLink email for identity verification.
|
|
832
|
-
* Includes
|
|
832
|
+
* Includes connectionId for WebSocket push notification of verification result.
|
|
833
833
|
*/
|
|
834
|
-
async sendSparkLink(email, authContext) {
|
|
834
|
+
async sendSparkLink(email, connectionId, authContext) {
|
|
835
835
|
return this.request('POST', '/sparklink/send', {
|
|
836
836
|
email,
|
|
837
837
|
type: 'verify_identity',
|
|
838
|
-
|
|
839
|
-
// This allows the ceremony page to notify the SDK directly instead of polling
|
|
840
|
-
openerOrigin: typeof window !== 'undefined' ? window.location.origin : undefined,
|
|
838
|
+
connection_id: connectionId,
|
|
841
839
|
...this.buildAuthContextParams(authContext),
|
|
842
840
|
});
|
|
843
841
|
}
|
|
844
|
-
/**
|
|
845
|
-
* Check SparkLink verification status (polling endpoint)
|
|
846
|
-
*/
|
|
847
|
-
async checkSparkLinkStatus(sparkId, _authContext) {
|
|
848
|
-
// For GET requests with auth context, we'd need query params, but the status endpoint
|
|
849
|
-
// doesn't need auth context - it's already stored in the sparklink record
|
|
850
|
-
return this.request('GET', `/sparklink/status/${sparkId}`);
|
|
851
|
-
}
|
|
852
842
|
}
|
|
853
843
|
class IdentityApiError extends Error {
|
|
854
844
|
constructor(message, code, statusCode) {
|
|
@@ -1490,7 +1480,7 @@ class TotpHandler {
|
|
|
1490
1480
|
success: false,
|
|
1491
1481
|
newKindling: response.kindling,
|
|
1492
1482
|
retryAfter: response.retry_after,
|
|
1493
|
-
backoffExpires: response.
|
|
1483
|
+
backoffExpires: response.retry_after, // When backoff ends (not code expiry)
|
|
1494
1484
|
error: 'Invalid code. Please try again.',
|
|
1495
1485
|
};
|
|
1496
1486
|
}
|
|
@@ -1507,11 +1497,11 @@ class TotpHandler {
|
|
|
1507
1497
|
/**
|
|
1508
1498
|
* SparkLink Handler
|
|
1509
1499
|
*
|
|
1510
|
-
* Single responsibility: SparkLink sending
|
|
1511
|
-
*
|
|
1500
|
+
* Single responsibility: SparkLink sending.
|
|
1501
|
+
* Verification is handled via WebSocket push (no polling, no iframe).
|
|
1512
1502
|
*/
|
|
1513
1503
|
/**
|
|
1514
|
-
* Handles SparkLink sending
|
|
1504
|
+
* Handles SparkLink sending
|
|
1515
1505
|
*/
|
|
1516
1506
|
class SparkLinkHandler {
|
|
1517
1507
|
constructor(api, state) {
|
|
@@ -1520,10 +1510,11 @@ class SparkLinkHandler {
|
|
|
1520
1510
|
}
|
|
1521
1511
|
/**
|
|
1522
1512
|
* Send SparkLink to the user's email
|
|
1513
|
+
* @param connectionId - WebSocket connectionId for push notification
|
|
1523
1514
|
*/
|
|
1524
|
-
async send() {
|
|
1515
|
+
async send(connectionId) {
|
|
1525
1516
|
try {
|
|
1526
|
-
const result = await this.api.sendSparkLink(this.state.recipient, this.state.authContext);
|
|
1517
|
+
const result = await this.api.sendSparkLink(this.state.recipient, connectionId, this.state.authContext);
|
|
1527
1518
|
return {
|
|
1528
1519
|
success: true,
|
|
1529
1520
|
sparkId: result.sparkId,
|
|
@@ -1538,26 +1529,6 @@ class SparkLinkHandler {
|
|
|
1538
1529
|
};
|
|
1539
1530
|
}
|
|
1540
1531
|
}
|
|
1541
|
-
/**
|
|
1542
|
-
* Check SparkLink verification status
|
|
1543
|
-
* Called periodically by the renderer to poll for completion
|
|
1544
|
-
*/
|
|
1545
|
-
async checkStatus(sparkId) {
|
|
1546
|
-
try {
|
|
1547
|
-
const result = await this.api.checkSparkLinkStatus(sparkId);
|
|
1548
|
-
return {
|
|
1549
|
-
verified: result.verified,
|
|
1550
|
-
token: result.token,
|
|
1551
|
-
identity: result.identity,
|
|
1552
|
-
identityType: result.identityType,
|
|
1553
|
-
redirect: result.redirect,
|
|
1554
|
-
};
|
|
1555
|
-
}
|
|
1556
|
-
catch {
|
|
1557
|
-
// On error, return not verified (polling will continue)
|
|
1558
|
-
return { verified: false };
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
1532
|
}
|
|
1562
1533
|
|
|
1563
1534
|
/**
|
|
@@ -1686,6 +1657,7 @@ function getStyles(options) {
|
|
|
1686
1657
|
padding: 14px 20px;
|
|
1687
1658
|
border-bottom: 1px solid ${tokens.border};
|
|
1688
1659
|
background: ${tokens.bg};
|
|
1660
|
+
border-radius: 16px 16px 0 0;
|
|
1689
1661
|
flex-shrink: 0;
|
|
1690
1662
|
}
|
|
1691
1663
|
|
|
@@ -4229,7 +4201,7 @@ class SparkLinkWaitingView {
|
|
|
4229
4201
|
* Switch to expired state - static icon, no animation, helpful message
|
|
4230
4202
|
*/
|
|
4231
4203
|
showExpiredState() {
|
|
4232
|
-
// Notify renderer to
|
|
4204
|
+
// Notify renderer to clean up WebSocket connection
|
|
4233
4205
|
this.props.onExpired();
|
|
4234
4206
|
// Update waiting section: replace spinner with expired icon
|
|
4235
4207
|
if (this.waitingSection) {
|
|
@@ -4376,9 +4348,8 @@ class IdentityRenderer {
|
|
|
4376
4348
|
// View management
|
|
4377
4349
|
this.currentView = null;
|
|
4378
4350
|
this.focusTimeoutId = null;
|
|
4379
|
-
// SparkLink
|
|
4380
|
-
this.
|
|
4381
|
-
this.messageListener = null;
|
|
4351
|
+
// SparkLink WebSocket
|
|
4352
|
+
this.sparkLinkWs = null;
|
|
4382
4353
|
this.container = container;
|
|
4383
4354
|
this.api = api;
|
|
4384
4355
|
this.options = options;
|
|
@@ -4477,7 +4448,7 @@ class IdentityRenderer {
|
|
|
4477
4448
|
clearTimeout(this.focusTimeoutId);
|
|
4478
4449
|
this.focusTimeoutId = null;
|
|
4479
4450
|
}
|
|
4480
|
-
this.
|
|
4451
|
+
this.cleanupSparkLink();
|
|
4481
4452
|
this.destroyCurrentView();
|
|
4482
4453
|
this.container.destroy();
|
|
4483
4454
|
}
|
|
@@ -4582,7 +4553,7 @@ class IdentityRenderer {
|
|
|
4582
4553
|
onResend: () => this.handleSparkLinkResend(),
|
|
4583
4554
|
onFallback: () => this.handleSparkLinkFallback(),
|
|
4584
4555
|
onBack: () => this.showMethodSelect(),
|
|
4585
|
-
onExpired: () => this.
|
|
4556
|
+
onExpired: () => this.cleanupSparkLink(),
|
|
4586
4557
|
});
|
|
4587
4558
|
case 'oauth-pending':
|
|
4588
4559
|
return new LoadingView({ message: `Connecting to ${state.provider}...` });
|
|
@@ -4712,8 +4683,20 @@ class IdentityRenderer {
|
|
|
4712
4683
|
}
|
|
4713
4684
|
case 'sparklink': {
|
|
4714
4685
|
this.setState({ view: 'loading' });
|
|
4715
|
-
|
|
4686
|
+
// Open WebSocket and get connectionId before sending
|
|
4687
|
+
const connectionId = await this.openSparkLinkWebSocket();
|
|
4688
|
+
if (!connectionId) {
|
|
4689
|
+
this.cleanupSparkLink();
|
|
4690
|
+
this.setState({
|
|
4691
|
+
view: 'error',
|
|
4692
|
+
message: 'Failed to establish connection',
|
|
4693
|
+
code: 'sparklink_ws_failed',
|
|
4694
|
+
});
|
|
4695
|
+
return;
|
|
4696
|
+
}
|
|
4697
|
+
const result = await this.sparkLinkHandler.send(connectionId);
|
|
4716
4698
|
if (!result.success) {
|
|
4699
|
+
this.cleanupSparkLink();
|
|
4717
4700
|
this.setState({
|
|
4718
4701
|
view: 'error',
|
|
4719
4702
|
message: result.error ?? 'Failed to send SparkLink',
|
|
@@ -4724,10 +4707,8 @@ class IdentityRenderer {
|
|
|
4724
4707
|
this.setState({
|
|
4725
4708
|
view: 'sparklink-waiting',
|
|
4726
4709
|
email: this.recipient,
|
|
4727
|
-
sparkId: result.sparkId,
|
|
4728
4710
|
expiresAt: result.expiresAt,
|
|
4729
4711
|
});
|
|
4730
|
-
this.startSparkLinkPolling(result.sparkId);
|
|
4731
4712
|
break;
|
|
4732
4713
|
}
|
|
4733
4714
|
case 'social': {
|
|
@@ -5013,51 +4994,110 @@ class IdentityRenderer {
|
|
|
5013
4994
|
this.callbacks.onSuccess(pendingResult);
|
|
5014
4995
|
}
|
|
5015
4996
|
/**
|
|
5016
|
-
*
|
|
5017
|
-
*
|
|
5018
|
-
*
|
|
4997
|
+
* Open a WebSocket connection and return the connectionId.
|
|
4998
|
+
* The server echoes back the connectionId on init; this is used to route
|
|
4999
|
+
* the SparkLink verification result back to this SDK instance.
|
|
5019
5000
|
*/
|
|
5020
|
-
|
|
5021
|
-
this.
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5001
|
+
openSparkLinkWebSocket() {
|
|
5002
|
+
this.cleanupSparkLink();
|
|
5003
|
+
const wsDomain = this.sdkConfig?.wsDomain || 'ws.sparkvault.com';
|
|
5004
|
+
const wsUrl = `wss://${wsDomain}`;
|
|
5005
|
+
return new Promise((resolve) => {
|
|
5006
|
+
let resolved = false;
|
|
5007
|
+
const ws = new WebSocket(wsUrl);
|
|
5008
|
+
this.sparkLinkWs = ws;
|
|
5009
|
+
const timeout = setTimeout(() => {
|
|
5010
|
+
if (!resolved) {
|
|
5011
|
+
resolved = true;
|
|
5012
|
+
ws.close();
|
|
5013
|
+
resolve(null);
|
|
5014
|
+
}
|
|
5015
|
+
}, 10000);
|
|
5016
|
+
ws.onopen = () => {
|
|
5017
|
+
ws.send(JSON.stringify({ action: 'init' }));
|
|
5018
|
+
};
|
|
5019
|
+
ws.onmessage = (event) => {
|
|
5020
|
+
let data;
|
|
5021
|
+
try {
|
|
5022
|
+
data = JSON.parse(event.data);
|
|
5023
|
+
}
|
|
5024
|
+
catch {
|
|
5025
|
+
return;
|
|
5026
|
+
}
|
|
5027
|
+
if (!resolved && data.type === 'connection' && typeof data.connectionId === 'string') {
|
|
5028
|
+
resolved = true;
|
|
5029
|
+
clearTimeout(timeout);
|
|
5030
|
+
this.setupSparkLinkWsHandler(ws);
|
|
5031
|
+
resolve(data.connectionId);
|
|
5032
|
+
return;
|
|
5033
|
+
}
|
|
5034
|
+
};
|
|
5035
|
+
ws.onerror = () => {
|
|
5036
|
+
if (!resolved) {
|
|
5037
|
+
resolved = true;
|
|
5038
|
+
clearTimeout(timeout);
|
|
5039
|
+
resolve(null);
|
|
5040
|
+
}
|
|
5041
|
+
};
|
|
5042
|
+
ws.onclose = () => {
|
|
5043
|
+
if (!resolved) {
|
|
5044
|
+
resolved = true;
|
|
5045
|
+
clearTimeout(timeout);
|
|
5046
|
+
resolve(null);
|
|
5047
|
+
}
|
|
5048
|
+
};
|
|
5049
|
+
});
|
|
5050
|
+
}
|
|
5051
|
+
/**
|
|
5052
|
+
* Set up WebSocket message handler for SparkLink verification results.
|
|
5053
|
+
* Handles: sparklink_verified, sparklink_failed, sparklink_resent.
|
|
5054
|
+
*/
|
|
5055
|
+
setupSparkLinkWsHandler(ws) {
|
|
5056
|
+
ws.onmessage = (event) => {
|
|
5057
|
+
let data;
|
|
5058
|
+
try {
|
|
5059
|
+
data = JSON.parse(event.data);
|
|
5060
|
+
}
|
|
5061
|
+
catch {
|
|
5033
5062
|
return;
|
|
5034
|
-
this.handleSparkLinkVerified({
|
|
5035
|
-
token,
|
|
5036
|
-
identity,
|
|
5037
|
-
identityType: identityType || 'email',
|
|
5038
|
-
redirect,
|
|
5039
|
-
});
|
|
5040
|
-
};
|
|
5041
|
-
window.addEventListener('message', this.messageListener);
|
|
5042
|
-
// Fallback: Poll status endpoint every 2 seconds
|
|
5043
|
-
// This catches cases where postMessage might not work (popup blockers, etc)
|
|
5044
|
-
this.pollingInterval = setInterval(async () => {
|
|
5045
|
-
const status = await this.sparkLinkHandler.checkStatus(sparkId);
|
|
5046
|
-
if (status.verified && status.token && status.identity) {
|
|
5047
|
-
this.handleSparkLinkVerified({
|
|
5048
|
-
token: status.token,
|
|
5049
|
-
identity: status.identity,
|
|
5050
|
-
identityType: status.identityType || 'email',
|
|
5051
|
-
redirect: status.redirect,
|
|
5052
|
-
});
|
|
5053
5063
|
}
|
|
5054
|
-
|
|
5064
|
+
switch (data.type) {
|
|
5065
|
+
case 'sparklink_verified':
|
|
5066
|
+
this.handleSparkLinkVerified({
|
|
5067
|
+
token: data.token,
|
|
5068
|
+
identity: data.identity,
|
|
5069
|
+
identityType: data.identityType || 'email',
|
|
5070
|
+
});
|
|
5071
|
+
break;
|
|
5072
|
+
case 'sparklink_failed':
|
|
5073
|
+
this.cleanupSparkLink();
|
|
5074
|
+
this.setState({
|
|
5075
|
+
view: 'error',
|
|
5076
|
+
message: 'Verification failed. Please try again or choose a different method.',
|
|
5077
|
+
code: 'sparklink_failed',
|
|
5078
|
+
});
|
|
5079
|
+
break;
|
|
5080
|
+
case 'sparklink_resent':
|
|
5081
|
+
// Server auto-resent a new link — update countdown timer
|
|
5082
|
+
if (typeof data.expiresAt === 'number') {
|
|
5083
|
+
this.setState({
|
|
5084
|
+
view: 'sparklink-waiting',
|
|
5085
|
+
email: this.recipient,
|
|
5086
|
+
expiresAt: data.expiresAt,
|
|
5087
|
+
});
|
|
5088
|
+
}
|
|
5089
|
+
break;
|
|
5090
|
+
}
|
|
5091
|
+
};
|
|
5092
|
+
ws.onclose = () => {
|
|
5093
|
+
this.sparkLinkWs = null;
|
|
5094
|
+
};
|
|
5055
5095
|
}
|
|
5056
5096
|
/**
|
|
5057
|
-
* Handle successful SparkLink verification
|
|
5097
|
+
* Handle successful SparkLink verification via WebSocket push.
|
|
5058
5098
|
*/
|
|
5059
5099
|
async handleSparkLinkVerified(result) {
|
|
5060
|
-
this.
|
|
5100
|
+
this.cleanupSparkLink();
|
|
5061
5101
|
// Handle redirect for OIDC/simple mode flows
|
|
5062
5102
|
if (result.redirect) {
|
|
5063
5103
|
this.close();
|
|
@@ -5077,39 +5117,44 @@ class IdentityRenderer {
|
|
|
5077
5117
|
this.callbacks.onSuccess(result);
|
|
5078
5118
|
}
|
|
5079
5119
|
/**
|
|
5080
|
-
*
|
|
5120
|
+
* Clean up SparkLink WebSocket connection.
|
|
5081
5121
|
*/
|
|
5082
|
-
|
|
5083
|
-
if (this.
|
|
5084
|
-
|
|
5085
|
-
this.
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
this.messageListener = null;
|
|
5122
|
+
cleanupSparkLink() {
|
|
5123
|
+
if (this.sparkLinkWs) {
|
|
5124
|
+
this.sparkLinkWs.onmessage = null;
|
|
5125
|
+
this.sparkLinkWs.onclose = null;
|
|
5126
|
+
this.sparkLinkWs.onerror = null;
|
|
5127
|
+
this.sparkLinkWs.close();
|
|
5128
|
+
this.sparkLinkWs = null;
|
|
5090
5129
|
}
|
|
5091
5130
|
}
|
|
5092
5131
|
/**
|
|
5093
5132
|
* Handle resending SparkLink.
|
|
5094
5133
|
*/
|
|
5095
5134
|
async handleSparkLinkResend() {
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5135
|
+
// Open new WebSocket for the resend
|
|
5136
|
+
const connectionId = await this.openSparkLinkWebSocket();
|
|
5137
|
+
if (!connectionId) {
|
|
5138
|
+
this.cleanupSparkLink();
|
|
5139
|
+
return;
|
|
5140
|
+
}
|
|
5141
|
+
const result = await this.sparkLinkHandler.send(connectionId);
|
|
5142
|
+
if (result.success) {
|
|
5099
5143
|
this.setState({
|
|
5100
5144
|
view: 'sparklink-waiting',
|
|
5101
5145
|
email: this.recipient,
|
|
5102
|
-
sparkId: result.sparkId,
|
|
5103
5146
|
expiresAt: result.expiresAt,
|
|
5104
5147
|
});
|
|
5105
|
-
|
|
5148
|
+
}
|
|
5149
|
+
else {
|
|
5150
|
+
this.cleanupSparkLink();
|
|
5106
5151
|
}
|
|
5107
5152
|
}
|
|
5108
5153
|
/**
|
|
5109
5154
|
* Handle fallback from SparkLink to TOTP verification code.
|
|
5110
5155
|
*/
|
|
5111
5156
|
async handleSparkLinkFallback() {
|
|
5112
|
-
this.
|
|
5157
|
+
this.cleanupSparkLink();
|
|
5113
5158
|
this.setState({ view: 'loading' });
|
|
5114
5159
|
const result = await this.totpHandler.send('email');
|
|
5115
5160
|
if (!result.success) {
|