@sparkvault/sdk 1.23.2 → 1.23.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.
@@ -122,6 +122,12 @@ export declare class IdentityRenderer {
122
122
  private setupSparkLinkWsHandler;
123
123
  /**
124
124
  * Handle successful SparkLink verification via WebSocket push.
125
+ *
126
+ * Always invokes the host callback with the full result (including any
127
+ * redirect URL) before attempting top-level navigation. The host page
128
+ * is the source of truth for what should happen after verification:
129
+ * the SDK's window.location.href call is a fallback for hosts that
130
+ * delegate navigation entirely to the SDK.
125
131
  */
126
132
  private handleSparkLinkVerified;
127
133
  /**
@@ -1967,6 +1967,21 @@ function getStyles(options) {
1967
1967
  text-align: left;
1968
1968
  padding: 12px 14px;
1969
1969
  margin-bottom: 8px;
1970
+ /* Inset 2px on either side so the focus outline (drawn outside the
1971
+ border-box) has room to render without clipping against the
1972
+ dialog's container edges. width:auto pairs with this to prevent
1973
+ the row from forcing horizontal overflow. */
1974
+ margin-left: 2px;
1975
+ margin-right: 2px;
1976
+ width: calc(100% - 4px);
1977
+ }
1978
+
1979
+ /* The shared .sv-btn focus rule sets outline-offset:2px which sits
1980
+ outside the row's edges and gets clipped by the parent. Method
1981
+ rows are full-width by design, so render the focus indicator
1982
+ flush against the border instead. */
1983
+ .sv-btn-method:focus-visible {
1984
+ outline-offset: 0;
1970
1985
  }
1971
1986
 
1972
1987
  .sv-btn-method:hover:not(:disabled) {
@@ -5450,14 +5465,9 @@ class IdentityRenderer {
5450
5465
  const currentMethod = this.verificationState.totp.method ?? 'email';
5451
5466
  const result = await this.totpHandler.verify(code);
5452
5467
  if (result.success && result.result) {
5453
- // Handle redirect for OIDC/simple mode flows
5454
- if (result.result.redirect) {
5455
- this.close();
5456
- window.location.href = result.result.redirect;
5457
- return;
5458
- }
5459
- // Check if we should prompt for passkey registration
5460
- if (await this.shouldShowPasskeyPrompt()) {
5468
+ // Passkey upsell only when there's no redirect target with one,
5469
+ // we head to the destination immediately.
5470
+ if (!result.result.redirect && await this.shouldShowPasskeyPrompt()) {
5461
5471
  this.setState({
5462
5472
  view: 'passkey-prompt',
5463
5473
  email: this.recipient,
@@ -5466,7 +5476,18 @@ class IdentityRenderer {
5466
5476
  return;
5467
5477
  }
5468
5478
  this.close();
5469
- this.callbacks.onSuccess(result.result);
5479
+ // Hand the full result (including redirect) to the host first so the
5480
+ // page-level handler can act before we navigate away.
5481
+ try {
5482
+ this.callbacks.onSuccess(result.result);
5483
+ }
5484
+ catch (err) {
5485
+ // eslint-disable-next-line no-console
5486
+ console.warn('[SparkVault SDK] onSuccess threw, continuing to redirect', err);
5487
+ }
5488
+ if (typeof result.result.redirect === 'string' && result.result.redirect) {
5489
+ window.location.href = result.result.redirect;
5490
+ }
5470
5491
  }
5471
5492
  else {
5472
5493
  // Check if error is expiry - auto-resend if so
@@ -5529,14 +5550,17 @@ class IdentityRenderer {
5529
5550
  ? await this.passkeyHandler.register()
5530
5551
  : await this.passkeyHandler.verify();
5531
5552
  if (result.success && result.result) {
5532
- // Handle redirect for OIDC/simple mode flows
5533
- if (result.result.redirect) {
5534
- this.close();
5553
+ this.close();
5554
+ try {
5555
+ this.callbacks.onSuccess(result.result);
5556
+ }
5557
+ catch (err) {
5558
+ // eslint-disable-next-line no-console
5559
+ console.warn('[SparkVault SDK] onSuccess threw, continuing to redirect', err);
5560
+ }
5561
+ if (typeof result.result.redirect === 'string' && result.result.redirect) {
5535
5562
  window.location.href = result.result.redirect;
5536
- return;
5537
5563
  }
5538
- this.close();
5539
- this.callbacks.onSuccess(result.result);
5540
5564
  }
5541
5565
  else {
5542
5566
  // Handle different error types
@@ -5574,14 +5598,17 @@ class IdentityRenderer {
5574
5598
  const pendingResult = this.verificationState.passkey.pendingResult;
5575
5599
  if (pendingResult) {
5576
5600
  this.verificationState.setPendingResult(null);
5577
- // Handle redirect for OIDC/simple mode flows
5578
- if (pendingResult.redirect) {
5579
- this.close();
5601
+ this.close();
5602
+ try {
5603
+ this.callbacks.onSuccess(pendingResult);
5604
+ }
5605
+ catch (err) {
5606
+ // eslint-disable-next-line no-console
5607
+ console.warn('[SparkVault SDK] onSuccess threw, continuing to redirect', err);
5608
+ }
5609
+ if (typeof pendingResult.redirect === 'string' && pendingResult.redirect) {
5580
5610
  window.location.href = pendingResult.redirect;
5581
- return;
5582
5611
  }
5583
- this.close();
5584
- this.callbacks.onSuccess(pendingResult);
5585
5612
  }
5586
5613
  }
5587
5614
  handleSocialLogin(provider) {
@@ -5663,15 +5690,18 @@ class IdentityRenderer {
5663
5690
  // Directly trigger passkey registration
5664
5691
  const result = await this.passkeyHandler.register();
5665
5692
  if (result.success && result.result) {
5666
- // Handle redirect for OIDC/simple mode flows
5667
- if (result.result.redirect) {
5668
- this.close();
5669
- window.location.href = result.result.redirect;
5670
- return;
5671
- }
5672
5693
  // Passkey created successfully - use the new token
5673
5694
  this.close();
5674
- this.callbacks.onSuccess(result.result);
5695
+ try {
5696
+ this.callbacks.onSuccess(result.result);
5697
+ }
5698
+ catch (err) {
5699
+ // eslint-disable-next-line no-console
5700
+ console.warn('[SparkVault SDK] onSuccess threw, continuing to redirect', err);
5701
+ }
5702
+ if (typeof result.result.redirect === 'string' && result.result.redirect) {
5703
+ window.location.href = result.result.redirect;
5704
+ }
5675
5705
  }
5676
5706
  else if (result.errorType === 'cancelled') {
5677
5707
  // User cancelled browser dialog - go back to prompt so they can skip or try again
@@ -5698,15 +5728,18 @@ class IdentityRenderer {
5698
5728
  handlePasskeyPromptSkip(pendingResult) {
5699
5729
  // Set 30-day cookie to suppress future prompts
5700
5730
  this.verificationState.dismissPasskeyPrompt();
5701
- // Handle redirect for OIDC/simple mode flows
5702
- if (pendingResult.redirect) {
5703
- this.close();
5704
- window.location.href = pendingResult.redirect;
5705
- return;
5706
- }
5707
5731
  // Complete the verification
5708
5732
  this.close();
5709
- this.callbacks.onSuccess(pendingResult);
5733
+ try {
5734
+ this.callbacks.onSuccess(pendingResult);
5735
+ }
5736
+ catch (err) {
5737
+ // eslint-disable-next-line no-console
5738
+ console.warn('[SparkVault SDK] onSuccess threw, continuing to redirect', err);
5739
+ }
5740
+ if (typeof pendingResult.redirect === 'string' && pendingResult.redirect) {
5741
+ window.location.href = pendingResult.redirect;
5742
+ }
5710
5743
  }
5711
5744
  /**
5712
5745
  * Open a WebSocket connection and return the connectionId.
@@ -5808,6 +5841,19 @@ class IdentityRenderer {
5808
5841
  catch {
5809
5842
  return;
5810
5843
  }
5844
+ // Diagnostic: dump the parsed message so we can see on the client
5845
+ // EXACTLY what arrives. Keys + redirect prefix tell us whether the
5846
+ // field made it across API Gateway. Remove once we've confirmed.
5847
+ // eslint-disable-next-line no-console
5848
+ console.info('[SparkVault SDK] sparklink WS message', {
5849
+ type: data.type,
5850
+ keys: Object.keys(data),
5851
+ hasRedirect: typeof data.redirect === 'string',
5852
+ redirectPrefix: typeof data.redirect === 'string'
5853
+ ? data.redirect.slice(0, 80)
5854
+ : null,
5855
+ rawLength: event.data.length,
5856
+ });
5811
5857
  switch (data.type) {
5812
5858
  case 'sparklink_verified':
5813
5859
  this.handleSparkLinkVerified({
@@ -5846,17 +5892,18 @@ class IdentityRenderer {
5846
5892
  }
5847
5893
  /**
5848
5894
  * Handle successful SparkLink verification via WebSocket push.
5895
+ *
5896
+ * Always invokes the host callback with the full result (including any
5897
+ * redirect URL) before attempting top-level navigation. The host page
5898
+ * is the source of truth for what should happen after verification:
5899
+ * the SDK's window.location.href call is a fallback for hosts that
5900
+ * delegate navigation entirely to the SDK.
5849
5901
  */
5850
5902
  async handleSparkLinkVerified(result) {
5851
5903
  this.cleanupSparkLink();
5852
- // Handle redirect for OIDC/simple mode flows
5853
- if (result.redirect) {
5854
- this.close();
5855
- window.location.href = result.redirect;
5856
- return;
5857
- }
5858
- // Check if we should prompt for passkey registration
5859
- if (await this.shouldShowPasskeyPrompt()) {
5904
+ // Passkey upsell only when there's no redirect target once we have
5905
+ // a destination we go there immediately, no upsell.
5906
+ if (!result.redirect && await this.shouldShowPasskeyPrompt()) {
5860
5907
  this.setState({
5861
5908
  view: 'passkey-prompt',
5862
5909
  email: this.recipient,
@@ -5865,7 +5912,22 @@ class IdentityRenderer {
5865
5912
  return;
5866
5913
  }
5867
5914
  this.close();
5868
- this.callbacks.onSuccess(result);
5915
+ // Hand the full result (including redirect) to the host first so the
5916
+ // page-level handler can do app-specific work (logging, navigation,
5917
+ // etc.) before we steal the browsing context with a navigation.
5918
+ try {
5919
+ this.callbacks.onSuccess(result);
5920
+ }
5921
+ catch (err) {
5922
+ // eslint-disable-next-line no-console
5923
+ console.warn('[SparkVault SDK] onSuccess threw, continuing to redirect', err);
5924
+ }
5925
+ // SDK-side fallback navigation. If the host already navigated this is
5926
+ // a no-op (the document is already unloading); if not, this rescues
5927
+ // simple-mode flows whose host forgot to act on result.redirect.
5928
+ if (typeof result.redirect === 'string' && result.redirect) {
5929
+ window.location.href = result.redirect;
5930
+ }
5869
5931
  }
5870
5932
  /**
5871
5933
  * Clean up SparkLink WebSocket connection.