@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.
package/README.md CHANGED
@@ -16,7 +16,7 @@ The official SparkVault JavaScript SDK for browser applications. Add passwordles
16
16
  - [Error Handling](#error-handling)
17
17
  - [TypeScript Support](#typescript-support)
18
18
  - [Browser Support](#browser-support)
19
- - [Development](#development)
19
+ - [Build & Release](#build--release)
20
20
  - [License](#license)
21
21
 
22
22
  ## Installation
@@ -717,7 +717,7 @@ The SDK supports all modern browsers:
717
717
 
718
718
  **Note:** Passkey authentication requires WebAuthn support. On unsupported browsers, passkey will not appear as an option — other methods will still work.
719
719
 
720
- ## Development
720
+ ## Build & Release
721
721
 
722
722
  ### Setup
723
723
 
@@ -96,6 +96,11 @@ export declare class IdentityRenderer {
96
96
  * Open a WebSocket connection and return the connectionId.
97
97
  * The server echoes back the connectionId on init; this is used to route
98
98
  * the SparkLink verification result back to this SDK instance.
99
+ *
100
+ * Returns:
101
+ * - { connectionId: string } on success
102
+ * - { error: 'csp' } if blocked by Content Security Policy
103
+ * - { error: 'connection' } for other connection failures
99
104
  */
100
105
  private openSparkLinkWebSocket;
101
106
  /**
@@ -10,7 +10,7 @@ export { arrayBufferToBase64url, base64urlToArrayBuffer, base64urlToString, } fr
10
10
  /**
11
11
  * Extract the root (registrable) domain from a hostname for cross-subdomain cookies.
12
12
  * Returns the domain prefixed with a dot (e.g., ".client.com") or null when
13
- * the domain attribute should not be set (localhost, IP addresses).
13
+ * the domain attribute should not be set (loopback hosts, IP addresses).
14
14
  *
15
15
  * @param hostname - The hostname to extract the root domain from
16
16
  * @returns Root domain with leading dot, or null
@@ -565,13 +565,13 @@ const MULTI_PART_TLDS = new Set([
565
565
  /**
566
566
  * Extract the root (registrable) domain from a hostname for cross-subdomain cookies.
567
567
  * Returns the domain prefixed with a dot (e.g., ".client.com") or null when
568
- * the domain attribute should not be set (localhost, IP addresses).
568
+ * the domain attribute should not be set (loopback hosts, IP addresses).
569
569
  *
570
570
  * @param hostname - The hostname to extract the root domain from
571
571
  * @returns Root domain with leading dot, or null
572
572
  */
573
573
  function getRootDomain(hostname) {
574
- // Localhost — browsers reject domain attribute for localhost
574
+ // Loopback host — browsers reject domain attribute for localhost
575
575
  if (hostname === 'localhost' || hostname === '127.0.0.1')
576
576
  return null;
577
577
  // IPv4 addresses
@@ -1422,10 +1422,6 @@ class PasskeyHandler {
1422
1422
  if (hostname === 'sparkvault.com' || hostname.endsWith('.sparkvault.com')) {
1423
1423
  return true;
1424
1424
  }
1425
- // Allow localhost for development
1426
- if (hostname === 'localhost') {
1427
- return true;
1428
- }
1429
1425
  return false;
1430
1426
  }
1431
1427
  catch {
@@ -4794,17 +4790,27 @@ class IdentityRenderer {
4794
4790
  case 'sparklink': {
4795
4791
  this.setState({ view: 'loading' });
4796
4792
  // Open WebSocket and get connectionId before sending
4797
- const connectionId = await this.openSparkLinkWebSocket();
4798
- if (!connectionId) {
4793
+ const wsResult = await this.openSparkLinkWebSocket();
4794
+ if ('error' in wsResult) {
4799
4795
  this.cleanupSparkLink();
4800
- this.setState({
4801
- view: 'error',
4802
- message: 'Failed to establish connection',
4803
- code: 'sparklink_ws_failed',
4804
- });
4796
+ if (wsResult.error === 'csp') {
4797
+ // CSP blocking WebSocket - provide developer-friendly error
4798
+ this.setState({
4799
+ view: 'error',
4800
+ message: 'Connection blocked by security policy. Please contact the site administrator to add wss://*.sparkvault.com to the Content-Security-Policy connect-src directive.',
4801
+ code: 'sparklink_csp_blocked',
4802
+ });
4803
+ }
4804
+ else {
4805
+ this.setState({
4806
+ view: 'error',
4807
+ message: 'Failed to establish connection',
4808
+ code: 'sparklink_ws_failed',
4809
+ });
4810
+ }
4805
4811
  return;
4806
4812
  }
4807
- const result = await this.sparkLinkHandler.send(connectionId);
4813
+ const result = await this.sparkLinkHandler.send(wsResult.connectionId);
4808
4814
  if (!result.success) {
4809
4815
  this.cleanupSparkLink();
4810
4816
  this.setState({
@@ -5107,6 +5113,11 @@ class IdentityRenderer {
5107
5113
  * Open a WebSocket connection and return the connectionId.
5108
5114
  * The server echoes back the connectionId on init; this is used to route
5109
5115
  * the SparkLink verification result back to this SDK instance.
5116
+ *
5117
+ * Returns:
5118
+ * - { connectionId: string } on success
5119
+ * - { error: 'csp' } if blocked by Content Security Policy
5120
+ * - { error: 'connection' } for other connection failures
5110
5121
  */
5111
5122
  openSparkLinkWebSocket() {
5112
5123
  this.cleanupSparkLink();
@@ -5114,13 +5125,27 @@ class IdentityRenderer {
5114
5125
  const wsUrl = `wss://${wsDomain}`;
5115
5126
  return new Promise((resolve) => {
5116
5127
  let resolved = false;
5128
+ let cspViolationDetected = false;
5129
+ // Listen for CSP violations to provide a more helpful error message
5130
+ const cspHandler = (event) => {
5131
+ // Check if this CSP violation is for our WebSocket connection
5132
+ if (event.violatedDirective === 'connect-src' &&
5133
+ event.blockedURI.includes(wsDomain)) {
5134
+ cspViolationDetected = true;
5135
+ }
5136
+ };
5137
+ document.addEventListener('securitypolicyviolation', cspHandler);
5138
+ const cleanup = () => {
5139
+ document.removeEventListener('securitypolicyviolation', cspHandler);
5140
+ };
5117
5141
  const ws = new WebSocket(wsUrl);
5118
5142
  this.sparkLinkWs = ws;
5119
5143
  const timeout = setTimeout(() => {
5120
5144
  if (!resolved) {
5121
5145
  resolved = true;
5146
+ cleanup();
5122
5147
  ws.close();
5123
- resolve(null);
5148
+ resolve({ error: cspViolationDetected ? 'csp' : 'connection' });
5124
5149
  }
5125
5150
  }, 10000);
5126
5151
  ws.onopen = () => {
@@ -5130,8 +5155,9 @@ class IdentityRenderer {
5130
5155
  catch {
5131
5156
  if (!resolved) {
5132
5157
  resolved = true;
5158
+ cleanup();
5133
5159
  clearTimeout(timeout);
5134
- resolve(null);
5160
+ resolve({ error: 'connection' });
5135
5161
  }
5136
5162
  }
5137
5163
  };
@@ -5145,24 +5171,27 @@ class IdentityRenderer {
5145
5171
  }
5146
5172
  if (!resolved && data.type === 'connection' && typeof data.connectionId === 'string') {
5147
5173
  resolved = true;
5174
+ cleanup();
5148
5175
  clearTimeout(timeout);
5149
5176
  this.setupSparkLinkWsHandler(ws);
5150
- resolve(data.connectionId);
5177
+ resolve({ connectionId: data.connectionId });
5151
5178
  return;
5152
5179
  }
5153
5180
  };
5154
5181
  ws.onerror = () => {
5155
5182
  if (!resolved) {
5156
5183
  resolved = true;
5184
+ cleanup();
5157
5185
  clearTimeout(timeout);
5158
- resolve(null);
5186
+ resolve({ error: cspViolationDetected ? 'csp' : 'connection' });
5159
5187
  }
5160
5188
  };
5161
5189
  ws.onclose = () => {
5162
5190
  if (!resolved) {
5163
5191
  resolved = true;
5192
+ cleanup();
5164
5193
  clearTimeout(timeout);
5165
- resolve(null);
5194
+ resolve({ error: cspViolationDetected ? 'csp' : 'connection' });
5166
5195
  }
5167
5196
  };
5168
5197
  });
@@ -5252,17 +5281,26 @@ class IdentityRenderer {
5252
5281
  */
5253
5282
  async handleSparkLinkResend() {
5254
5283
  // Open new WebSocket for the resend
5255
- const connectionId = await this.openSparkLinkWebSocket();
5256
- if (!connectionId) {
5284
+ const wsResult = await this.openSparkLinkWebSocket();
5285
+ if ('error' in wsResult) {
5257
5286
  this.cleanupSparkLink();
5258
- this.setState({
5259
- view: 'error',
5260
- message: 'Unable to establish a secure connection. Please try again or choose a different method.',
5261
- code: 'sparklink_ws_failed',
5262
- });
5287
+ if (wsResult.error === 'csp') {
5288
+ this.setState({
5289
+ view: 'error',
5290
+ message: 'Connection blocked by security policy. Please contact the site administrator to add wss://*.sparkvault.com to the Content-Security-Policy connect-src directive.',
5291
+ code: 'sparklink_csp_blocked',
5292
+ });
5293
+ }
5294
+ else {
5295
+ this.setState({
5296
+ view: 'error',
5297
+ message: 'Unable to establish a secure connection. Please try again or choose a different method.',
5298
+ code: 'sparklink_ws_failed',
5299
+ });
5300
+ }
5263
5301
  return;
5264
5302
  }
5265
- const result = await this.sparkLinkHandler.send(connectionId);
5303
+ const result = await this.sparkLinkHandler.send(wsResult.connectionId);
5266
5304
  if (result.success) {
5267
5305
  this.setState({
5268
5306
  view: 'sparklink-waiting',
@@ -9221,7 +9259,6 @@ if (typeof window !== 'undefined') {
9221
9259
  // Auto-initialize from script tag data-account-id attribute
9222
9260
  const instance = autoInit(SparkVault);
9223
9261
  // Expose instance (if auto-init succeeded) or class (for manual init)
9224
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9225
9262
  window.SparkVault = instance ?? SparkVault;
9226
9263
  }
9227
9264