@sparkvault/sdk 1.23.2 → 1.23.3

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
  /**
@@ -5450,14 +5450,9 @@ class IdentityRenderer {
5450
5450
  const currentMethod = this.verificationState.totp.method ?? 'email';
5451
5451
  const result = await this.totpHandler.verify(code);
5452
5452
  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()) {
5453
+ // Passkey upsell only when there's no redirect target with one,
5454
+ // we head to the destination immediately.
5455
+ if (!result.result.redirect && await this.shouldShowPasskeyPrompt()) {
5461
5456
  this.setState({
5462
5457
  view: 'passkey-prompt',
5463
5458
  email: this.recipient,
@@ -5466,7 +5461,18 @@ class IdentityRenderer {
5466
5461
  return;
5467
5462
  }
5468
5463
  this.close();
5469
- this.callbacks.onSuccess(result.result);
5464
+ // Hand the full result (including redirect) to the host first so the
5465
+ // page-level handler can act before we navigate away.
5466
+ try {
5467
+ this.callbacks.onSuccess(result.result);
5468
+ }
5469
+ catch (err) {
5470
+ // eslint-disable-next-line no-console
5471
+ console.warn('[SparkVault SDK] onSuccess threw, continuing to redirect', err);
5472
+ }
5473
+ if (typeof result.result.redirect === 'string' && result.result.redirect) {
5474
+ window.location.href = result.result.redirect;
5475
+ }
5470
5476
  }
5471
5477
  else {
5472
5478
  // Check if error is expiry - auto-resend if so
@@ -5529,14 +5535,17 @@ class IdentityRenderer {
5529
5535
  ? await this.passkeyHandler.register()
5530
5536
  : await this.passkeyHandler.verify();
5531
5537
  if (result.success && result.result) {
5532
- // Handle redirect for OIDC/simple mode flows
5533
- if (result.result.redirect) {
5534
- this.close();
5538
+ this.close();
5539
+ try {
5540
+ this.callbacks.onSuccess(result.result);
5541
+ }
5542
+ catch (err) {
5543
+ // eslint-disable-next-line no-console
5544
+ console.warn('[SparkVault SDK] onSuccess threw, continuing to redirect', err);
5545
+ }
5546
+ if (typeof result.result.redirect === 'string' && result.result.redirect) {
5535
5547
  window.location.href = result.result.redirect;
5536
- return;
5537
5548
  }
5538
- this.close();
5539
- this.callbacks.onSuccess(result.result);
5540
5549
  }
5541
5550
  else {
5542
5551
  // Handle different error types
@@ -5574,14 +5583,17 @@ class IdentityRenderer {
5574
5583
  const pendingResult = this.verificationState.passkey.pendingResult;
5575
5584
  if (pendingResult) {
5576
5585
  this.verificationState.setPendingResult(null);
5577
- // Handle redirect for OIDC/simple mode flows
5578
- if (pendingResult.redirect) {
5579
- this.close();
5586
+ this.close();
5587
+ try {
5588
+ this.callbacks.onSuccess(pendingResult);
5589
+ }
5590
+ catch (err) {
5591
+ // eslint-disable-next-line no-console
5592
+ console.warn('[SparkVault SDK] onSuccess threw, continuing to redirect', err);
5593
+ }
5594
+ if (typeof pendingResult.redirect === 'string' && pendingResult.redirect) {
5580
5595
  window.location.href = pendingResult.redirect;
5581
- return;
5582
5596
  }
5583
- this.close();
5584
- this.callbacks.onSuccess(pendingResult);
5585
5597
  }
5586
5598
  }
5587
5599
  handleSocialLogin(provider) {
@@ -5663,15 +5675,18 @@ class IdentityRenderer {
5663
5675
  // Directly trigger passkey registration
5664
5676
  const result = await this.passkeyHandler.register();
5665
5677
  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
5678
  // Passkey created successfully - use the new token
5673
5679
  this.close();
5674
- this.callbacks.onSuccess(result.result);
5680
+ try {
5681
+ this.callbacks.onSuccess(result.result);
5682
+ }
5683
+ catch (err) {
5684
+ // eslint-disable-next-line no-console
5685
+ console.warn('[SparkVault SDK] onSuccess threw, continuing to redirect', err);
5686
+ }
5687
+ if (typeof result.result.redirect === 'string' && result.result.redirect) {
5688
+ window.location.href = result.result.redirect;
5689
+ }
5675
5690
  }
5676
5691
  else if (result.errorType === 'cancelled') {
5677
5692
  // User cancelled browser dialog - go back to prompt so they can skip or try again
@@ -5698,15 +5713,18 @@ class IdentityRenderer {
5698
5713
  handlePasskeyPromptSkip(pendingResult) {
5699
5714
  // Set 30-day cookie to suppress future prompts
5700
5715
  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
5716
  // Complete the verification
5708
5717
  this.close();
5709
- this.callbacks.onSuccess(pendingResult);
5718
+ try {
5719
+ this.callbacks.onSuccess(pendingResult);
5720
+ }
5721
+ catch (err) {
5722
+ // eslint-disable-next-line no-console
5723
+ console.warn('[SparkVault SDK] onSuccess threw, continuing to redirect', err);
5724
+ }
5725
+ if (typeof pendingResult.redirect === 'string' && pendingResult.redirect) {
5726
+ window.location.href = pendingResult.redirect;
5727
+ }
5710
5728
  }
5711
5729
  /**
5712
5730
  * Open a WebSocket connection and return the connectionId.
@@ -5808,6 +5826,19 @@ class IdentityRenderer {
5808
5826
  catch {
5809
5827
  return;
5810
5828
  }
5829
+ // Diagnostic: dump the parsed message so we can see on the client
5830
+ // EXACTLY what arrives. Keys + redirect prefix tell us whether the
5831
+ // field made it across API Gateway. Remove once we've confirmed.
5832
+ // eslint-disable-next-line no-console
5833
+ console.info('[SparkVault SDK] sparklink WS message', {
5834
+ type: data.type,
5835
+ keys: Object.keys(data),
5836
+ hasRedirect: typeof data.redirect === 'string',
5837
+ redirectPrefix: typeof data.redirect === 'string'
5838
+ ? data.redirect.slice(0, 80)
5839
+ : null,
5840
+ rawLength: event.data.length,
5841
+ });
5811
5842
  switch (data.type) {
5812
5843
  case 'sparklink_verified':
5813
5844
  this.handleSparkLinkVerified({
@@ -5846,17 +5877,18 @@ class IdentityRenderer {
5846
5877
  }
5847
5878
  /**
5848
5879
  * Handle successful SparkLink verification via WebSocket push.
5880
+ *
5881
+ * Always invokes the host callback with the full result (including any
5882
+ * redirect URL) before attempting top-level navigation. The host page
5883
+ * is the source of truth for what should happen after verification:
5884
+ * the SDK's window.location.href call is a fallback for hosts that
5885
+ * delegate navigation entirely to the SDK.
5849
5886
  */
5850
5887
  async handleSparkLinkVerified(result) {
5851
5888
  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()) {
5889
+ // Passkey upsell only when there's no redirect target once we have
5890
+ // a destination we go there immediately, no upsell.
5891
+ if (!result.redirect && await this.shouldShowPasskeyPrompt()) {
5860
5892
  this.setState({
5861
5893
  view: 'passkey-prompt',
5862
5894
  email: this.recipient,
@@ -5865,7 +5897,22 @@ class IdentityRenderer {
5865
5897
  return;
5866
5898
  }
5867
5899
  this.close();
5868
- this.callbacks.onSuccess(result);
5900
+ // Hand the full result (including redirect) to the host first so the
5901
+ // page-level handler can do app-specific work (logging, navigation,
5902
+ // etc.) before we steal the browsing context with a navigation.
5903
+ try {
5904
+ this.callbacks.onSuccess(result);
5905
+ }
5906
+ catch (err) {
5907
+ // eslint-disable-next-line no-console
5908
+ console.warn('[SparkVault SDK] onSuccess threw, continuing to redirect', err);
5909
+ }
5910
+ // SDK-side fallback navigation. If the host already navigated this is
5911
+ // a no-op (the document is already unloading); if not, this rescues
5912
+ // simple-mode flows whose host forgot to act on result.redirect.
5913
+ if (typeof result.redirect === 'string' && result.redirect) {
5914
+ window.location.href = result.redirect;
5915
+ }
5869
5916
  }
5870
5917
  /**
5871
5918
  * Clean up SparkLink WebSocket connection.