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