@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/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
|
/**
|
|
@@ -4230,7 +4201,7 @@ class SparkLinkWaitingView {
|
|
|
4230
4201
|
* Switch to expired state - static icon, no animation, helpful message
|
|
4231
4202
|
*/
|
|
4232
4203
|
showExpiredState() {
|
|
4233
|
-
// Notify renderer to
|
|
4204
|
+
// Notify renderer to clean up WebSocket connection
|
|
4234
4205
|
this.props.onExpired();
|
|
4235
4206
|
// Update waiting section: replace spinner with expired icon
|
|
4236
4207
|
if (this.waitingSection) {
|
|
@@ -4377,9 +4348,8 @@ class IdentityRenderer {
|
|
|
4377
4348
|
// View management
|
|
4378
4349
|
this.currentView = null;
|
|
4379
4350
|
this.focusTimeoutId = null;
|
|
4380
|
-
// SparkLink
|
|
4381
|
-
this.
|
|
4382
|
-
this.messageListener = null;
|
|
4351
|
+
// SparkLink WebSocket
|
|
4352
|
+
this.sparkLinkWs = null;
|
|
4383
4353
|
this.container = container;
|
|
4384
4354
|
this.api = api;
|
|
4385
4355
|
this.options = options;
|
|
@@ -4478,7 +4448,7 @@ class IdentityRenderer {
|
|
|
4478
4448
|
clearTimeout(this.focusTimeoutId);
|
|
4479
4449
|
this.focusTimeoutId = null;
|
|
4480
4450
|
}
|
|
4481
|
-
this.
|
|
4451
|
+
this.cleanupSparkLink();
|
|
4482
4452
|
this.destroyCurrentView();
|
|
4483
4453
|
this.container.destroy();
|
|
4484
4454
|
}
|
|
@@ -4583,7 +4553,7 @@ class IdentityRenderer {
|
|
|
4583
4553
|
onResend: () => this.handleSparkLinkResend(),
|
|
4584
4554
|
onFallback: () => this.handleSparkLinkFallback(),
|
|
4585
4555
|
onBack: () => this.showMethodSelect(),
|
|
4586
|
-
onExpired: () => this.
|
|
4556
|
+
onExpired: () => this.cleanupSparkLink(),
|
|
4587
4557
|
});
|
|
4588
4558
|
case 'oauth-pending':
|
|
4589
4559
|
return new LoadingView({ message: `Connecting to ${state.provider}...` });
|
|
@@ -4713,8 +4683,20 @@ class IdentityRenderer {
|
|
|
4713
4683
|
}
|
|
4714
4684
|
case 'sparklink': {
|
|
4715
4685
|
this.setState({ view: 'loading' });
|
|
4716
|
-
|
|
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);
|
|
4717
4698
|
if (!result.success) {
|
|
4699
|
+
this.cleanupSparkLink();
|
|
4718
4700
|
this.setState({
|
|
4719
4701
|
view: 'error',
|
|
4720
4702
|
message: result.error ?? 'Failed to send SparkLink',
|
|
@@ -4725,10 +4707,8 @@ class IdentityRenderer {
|
|
|
4725
4707
|
this.setState({
|
|
4726
4708
|
view: 'sparklink-waiting',
|
|
4727
4709
|
email: this.recipient,
|
|
4728
|
-
sparkId: result.sparkId,
|
|
4729
4710
|
expiresAt: result.expiresAt,
|
|
4730
4711
|
});
|
|
4731
|
-
this.startSparkLinkPolling(result.sparkId);
|
|
4732
4712
|
break;
|
|
4733
4713
|
}
|
|
4734
4714
|
case 'social': {
|
|
@@ -5014,51 +4994,110 @@ class IdentityRenderer {
|
|
|
5014
4994
|
this.callbacks.onSuccess(pendingResult);
|
|
5015
4995
|
}
|
|
5016
4996
|
/**
|
|
5017
|
-
*
|
|
5018
|
-
*
|
|
5019
|
-
*
|
|
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.
|
|
5020
5000
|
*/
|
|
5021
|
-
|
|
5022
|
-
this.
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
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 {
|
|
5034
5062
|
return;
|
|
5035
|
-
this.handleSparkLinkVerified({
|
|
5036
|
-
token,
|
|
5037
|
-
identity,
|
|
5038
|
-
identityType: identityType || 'email',
|
|
5039
|
-
redirect,
|
|
5040
|
-
});
|
|
5041
|
-
};
|
|
5042
|
-
window.addEventListener('message', this.messageListener);
|
|
5043
|
-
// Fallback: Poll status endpoint every 2 seconds
|
|
5044
|
-
// This catches cases where postMessage might not work (popup blockers, etc)
|
|
5045
|
-
this.pollingInterval = setInterval(async () => {
|
|
5046
|
-
const status = await this.sparkLinkHandler.checkStatus(sparkId);
|
|
5047
|
-
if (status.verified && status.token && status.identity) {
|
|
5048
|
-
this.handleSparkLinkVerified({
|
|
5049
|
-
token: status.token,
|
|
5050
|
-
identity: status.identity,
|
|
5051
|
-
identityType: status.identityType || 'email',
|
|
5052
|
-
redirect: status.redirect,
|
|
5053
|
-
});
|
|
5054
5063
|
}
|
|
5055
|
-
|
|
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
|
+
};
|
|
5056
5095
|
}
|
|
5057
5096
|
/**
|
|
5058
|
-
* Handle successful SparkLink verification
|
|
5097
|
+
* Handle successful SparkLink verification via WebSocket push.
|
|
5059
5098
|
*/
|
|
5060
5099
|
async handleSparkLinkVerified(result) {
|
|
5061
|
-
this.
|
|
5100
|
+
this.cleanupSparkLink();
|
|
5062
5101
|
// Handle redirect for OIDC/simple mode flows
|
|
5063
5102
|
if (result.redirect) {
|
|
5064
5103
|
this.close();
|
|
@@ -5078,39 +5117,44 @@ class IdentityRenderer {
|
|
|
5078
5117
|
this.callbacks.onSuccess(result);
|
|
5079
5118
|
}
|
|
5080
5119
|
/**
|
|
5081
|
-
*
|
|
5120
|
+
* Clean up SparkLink WebSocket connection.
|
|
5082
5121
|
*/
|
|
5083
|
-
|
|
5084
|
-
if (this.
|
|
5085
|
-
|
|
5086
|
-
this.
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
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;
|
|
5091
5129
|
}
|
|
5092
5130
|
}
|
|
5093
5131
|
/**
|
|
5094
5132
|
* Handle resending SparkLink.
|
|
5095
5133
|
*/
|
|
5096
5134
|
async handleSparkLinkResend() {
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
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) {
|
|
5100
5143
|
this.setState({
|
|
5101
5144
|
view: 'sparklink-waiting',
|
|
5102
5145
|
email: this.recipient,
|
|
5103
|
-
sparkId: result.sparkId,
|
|
5104
5146
|
expiresAt: result.expiresAt,
|
|
5105
5147
|
});
|
|
5106
|
-
|
|
5148
|
+
}
|
|
5149
|
+
else {
|
|
5150
|
+
this.cleanupSparkLink();
|
|
5107
5151
|
}
|
|
5108
5152
|
}
|
|
5109
5153
|
/**
|
|
5110
5154
|
* Handle fallback from SparkLink to TOTP verification code.
|
|
5111
5155
|
*/
|
|
5112
5156
|
async handleSparkLinkFallback() {
|
|
5113
|
-
this.
|
|
5157
|
+
this.cleanupSparkLink();
|
|
5114
5158
|
this.setState({ view: 'loading' });
|
|
5115
5159
|
const result = await this.totpHandler.send('email');
|
|
5116
5160
|
if (!result.success) {
|