@sparkvault/sdk 1.21.9 → 1.21.11

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.
@@ -561,13 +561,13 @@ const MULTI_PART_TLDS = new Set([
561
561
  /**
562
562
  * Extract the root (registrable) domain from a hostname for cross-subdomain cookies.
563
563
  * Returns the domain prefixed with a dot (e.g., ".client.com") or null when
564
- * the domain attribute should not be set (localhost, IP addresses).
564
+ * the domain attribute should not be set (loopback hosts, IP addresses).
565
565
  *
566
566
  * @param hostname - The hostname to extract the root domain from
567
567
  * @returns Root domain with leading dot, or null
568
568
  */
569
569
  function getRootDomain(hostname) {
570
- // Localhost — browsers reject domain attribute for localhost
570
+ // Loopback host — browsers reject domain attribute for localhost
571
571
  if (hostname === 'localhost' || hostname === '127.0.0.1')
572
572
  return null;
573
573
  // IPv4 addresses
@@ -1418,10 +1418,6 @@ class PasskeyHandler {
1418
1418
  if (hostname === 'sparkvault.com' || hostname.endsWith('.sparkvault.com')) {
1419
1419
  return true;
1420
1420
  }
1421
- // Allow localhost for development
1422
- if (hostname === 'localhost') {
1423
- return true;
1424
- }
1425
1421
  return false;
1426
1422
  }
1427
1423
  catch {
@@ -4790,17 +4786,27 @@ class IdentityRenderer {
4790
4786
  case 'sparklink': {
4791
4787
  this.setState({ view: 'loading' });
4792
4788
  // Open WebSocket and get connectionId before sending
4793
- const connectionId = await this.openSparkLinkWebSocket();
4794
- if (!connectionId) {
4789
+ const wsResult = await this.openSparkLinkWebSocket();
4790
+ if ('error' in wsResult) {
4795
4791
  this.cleanupSparkLink();
4796
- this.setState({
4797
- view: 'error',
4798
- message: 'Failed to establish connection',
4799
- code: 'sparklink_ws_failed',
4800
- });
4792
+ if (wsResult.error === 'csp') {
4793
+ // CSP blocking WebSocket - provide developer-friendly error
4794
+ this.setState({
4795
+ view: 'error',
4796
+ message: 'Connection blocked by security policy. Please contact the site administrator to add wss://*.sparkvault.com to the Content-Security-Policy connect-src directive.',
4797
+ code: 'sparklink_csp_blocked',
4798
+ });
4799
+ }
4800
+ else {
4801
+ this.setState({
4802
+ view: 'error',
4803
+ message: 'Failed to establish connection',
4804
+ code: 'sparklink_ws_failed',
4805
+ });
4806
+ }
4801
4807
  return;
4802
4808
  }
4803
- const result = await this.sparkLinkHandler.send(connectionId);
4809
+ const result = await this.sparkLinkHandler.send(wsResult.connectionId);
4804
4810
  if (!result.success) {
4805
4811
  this.cleanupSparkLink();
4806
4812
  this.setState({
@@ -5103,6 +5109,11 @@ class IdentityRenderer {
5103
5109
  * Open a WebSocket connection and return the connectionId.
5104
5110
  * The server echoes back the connectionId on init; this is used to route
5105
5111
  * the SparkLink verification result back to this SDK instance.
5112
+ *
5113
+ * Returns:
5114
+ * - { connectionId: string } on success
5115
+ * - { error: 'csp' } if blocked by Content Security Policy
5116
+ * - { error: 'connection' } for other connection failures
5106
5117
  */
5107
5118
  openSparkLinkWebSocket() {
5108
5119
  this.cleanupSparkLink();
@@ -5110,13 +5121,27 @@ class IdentityRenderer {
5110
5121
  const wsUrl = `wss://${wsDomain}`;
5111
5122
  return new Promise((resolve) => {
5112
5123
  let resolved = false;
5124
+ let cspViolationDetected = false;
5125
+ // Listen for CSP violations to provide a more helpful error message
5126
+ const cspHandler = (event) => {
5127
+ // Check if this CSP violation is for our WebSocket connection
5128
+ if (event.violatedDirective === 'connect-src' &&
5129
+ event.blockedURI.includes(wsDomain)) {
5130
+ cspViolationDetected = true;
5131
+ }
5132
+ };
5133
+ document.addEventListener('securitypolicyviolation', cspHandler);
5134
+ const cleanup = () => {
5135
+ document.removeEventListener('securitypolicyviolation', cspHandler);
5136
+ };
5113
5137
  const ws = new WebSocket(wsUrl);
5114
5138
  this.sparkLinkWs = ws;
5115
5139
  const timeout = setTimeout(() => {
5116
5140
  if (!resolved) {
5117
5141
  resolved = true;
5142
+ cleanup();
5118
5143
  ws.close();
5119
- resolve(null);
5144
+ resolve({ error: cspViolationDetected ? 'csp' : 'connection' });
5120
5145
  }
5121
5146
  }, 10000);
5122
5147
  ws.onopen = () => {
@@ -5126,8 +5151,9 @@ class IdentityRenderer {
5126
5151
  catch {
5127
5152
  if (!resolved) {
5128
5153
  resolved = true;
5154
+ cleanup();
5129
5155
  clearTimeout(timeout);
5130
- resolve(null);
5156
+ resolve({ error: 'connection' });
5131
5157
  }
5132
5158
  }
5133
5159
  };
@@ -5141,24 +5167,27 @@ class IdentityRenderer {
5141
5167
  }
5142
5168
  if (!resolved && data.type === 'connection' && typeof data.connectionId === 'string') {
5143
5169
  resolved = true;
5170
+ cleanup();
5144
5171
  clearTimeout(timeout);
5145
5172
  this.setupSparkLinkWsHandler(ws);
5146
- resolve(data.connectionId);
5173
+ resolve({ connectionId: data.connectionId });
5147
5174
  return;
5148
5175
  }
5149
5176
  };
5150
5177
  ws.onerror = () => {
5151
5178
  if (!resolved) {
5152
5179
  resolved = true;
5180
+ cleanup();
5153
5181
  clearTimeout(timeout);
5154
- resolve(null);
5182
+ resolve({ error: cspViolationDetected ? 'csp' : 'connection' });
5155
5183
  }
5156
5184
  };
5157
5185
  ws.onclose = () => {
5158
5186
  if (!resolved) {
5159
5187
  resolved = true;
5188
+ cleanup();
5160
5189
  clearTimeout(timeout);
5161
- resolve(null);
5190
+ resolve({ error: cspViolationDetected ? 'csp' : 'connection' });
5162
5191
  }
5163
5192
  };
5164
5193
  });
@@ -5248,17 +5277,26 @@ class IdentityRenderer {
5248
5277
  */
5249
5278
  async handleSparkLinkResend() {
5250
5279
  // Open new WebSocket for the resend
5251
- const connectionId = await this.openSparkLinkWebSocket();
5252
- if (!connectionId) {
5280
+ const wsResult = await this.openSparkLinkWebSocket();
5281
+ if ('error' in wsResult) {
5253
5282
  this.cleanupSparkLink();
5254
- this.setState({
5255
- view: 'error',
5256
- message: 'Unable to establish a secure connection. Please try again or choose a different method.',
5257
- code: 'sparklink_ws_failed',
5258
- });
5283
+ if (wsResult.error === 'csp') {
5284
+ this.setState({
5285
+ view: 'error',
5286
+ message: 'Connection blocked by security policy. Please contact the site administrator to add wss://*.sparkvault.com to the Content-Security-Policy connect-src directive.',
5287
+ code: 'sparklink_csp_blocked',
5288
+ });
5289
+ }
5290
+ else {
5291
+ this.setState({
5292
+ view: 'error',
5293
+ message: 'Unable to establish a secure connection. Please try again or choose a different method.',
5294
+ code: 'sparklink_ws_failed',
5295
+ });
5296
+ }
5259
5297
  return;
5260
5298
  }
5261
- const result = await this.sparkLinkHandler.send(connectionId);
5299
+ const result = await this.sparkLinkHandler.send(wsResult.connectionId);
5262
5300
  if (result.success) {
5263
5301
  this.setState({
5264
5302
  view: 'sparklink-waiting',
@@ -9217,7 +9255,6 @@ if (typeof window !== 'undefined') {
9217
9255
  // Auto-initialize from script tag data-account-id attribute
9218
9256
  const instance = autoInit(SparkVault);
9219
9257
  // Expose instance (if auto-init succeeded) or class (for manual init)
9220
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9221
9258
  window.SparkVault = instance ?? SparkVault;
9222
9259
  }
9223
9260