@lumiapassport/ui-kit 1.16.0 → 1.16.2

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.
@@ -153,6 +153,8 @@ async function startOAuthFlow() {
153
153
  /**
154
154
  * Handle successful OAuth after backend redirect
155
155
  * Backend processes callback and redirects back with success=true
156
+ * If authCode is present, exchanges it for tokens (Safari ITP fix)
157
+ * Otherwise falls back to cookie-based verify (desktop browsers)
156
158
  */
157
159
  async function handleBackendSuccess() {
158
160
  try {
@@ -166,32 +168,88 @@ async function handleBackendSuccess() {
166
168
  throw new Error('Missing TSS URL. Check build-time configuration.');
167
169
  }
168
170
 
169
- // Verify the session was created by checking auth endpoint
170
- const verifyEndpoint = PROJECT_ID
171
- ? `${TSS_URL}/api/auth/verify?projectId=${encodeURIComponent(PROJECT_ID)}`
172
- : `${TSS_URL}/api/auth/verify`;
171
+ // Check for auth code (new flow for Safari ITP compatibility)
172
+ const authCode = urlParams.get('authCode');
173
+ let userData;
174
+ let tokens = null;
173
175
 
174
- const verifyResponse = await fetch(verifyEndpoint, {
175
- method: 'GET',
176
- credentials: 'include',
177
- });
176
+ if (authCode) {
177
+ // New flow: Exchange auth code for tokens (fixes Safari ITP issues)
178
+ console.log('[X OAuth] Auth code present, exchanging for tokens...');
179
+
180
+ const exchangeEndpoint = PROJECT_ID
181
+ ? `${TSS_URL}/api/auth/exchange-code?projectId=${encodeURIComponent(PROJECT_ID)}`
182
+ : `${TSS_URL}/api/auth/exchange-code`;
183
+
184
+ const exchangeResponse = await fetch(exchangeEndpoint, {
185
+ method: 'POST',
186
+ headers: { 'Content-Type': 'application/json' },
187
+ body: JSON.stringify({ code: authCode })
188
+ });
189
+
190
+ if (!exchangeResponse.ok) {
191
+ const errorData = await exchangeResponse.json().catch(() => ({}));
192
+ console.error('[X OAuth] Code exchange failed:', exchangeResponse.status, errorData);
193
+ throw new Error(errorData.error || 'Failed to exchange auth code');
194
+ }
195
+
196
+ const exchangeData = await exchangeResponse.json();
197
+ console.log('[X OAuth] Code exchange successful:', { userId: exchangeData.userId, hasKeyshare: exchangeData.hasKeyshare });
198
+
199
+ // Extract tokens for postMessage
200
+ tokens = {
201
+ accessToken: exchangeData.accessToken,
202
+ refreshToken: exchangeData.refreshToken
203
+ };
204
+
205
+ // Build userData in the same format as verify endpoint
206
+ userData = {
207
+ valid: true,
208
+ userId: exchangeData.userId,
209
+ sessionId: exchangeData.sessionId,
210
+ expiresIn: exchangeData.expiresIn,
211
+ hasKeyshare: exchangeData.hasKeyshare,
212
+ displayName: exchangeData.displayName,
213
+ providers: exchangeData.providers
214
+ };
215
+ } else {
216
+ // Legacy flow: Use cookies (works on desktop browsers)
217
+ console.log('[X OAuth] No auth code, falling back to cookie-based verify...');
178
218
 
179
- if (!verifyResponse.ok) {
180
- console.error('[X OAuth] Verify failed:', verifyResponse.status);
181
- throw new Error('Failed to verify authentication. Session may not be created.');
219
+ const verifyEndpoint = PROJECT_ID
220
+ ? `${TSS_URL}/api/auth/verify?projectId=${encodeURIComponent(PROJECT_ID)}`
221
+ : `${TSS_URL}/api/auth/verify`;
222
+
223
+ const verifyResponse = await fetch(verifyEndpoint, {
224
+ method: 'GET',
225
+ credentials: 'include',
226
+ });
227
+
228
+ if (!verifyResponse.ok) {
229
+ console.error('[X OAuth] Verify failed:', verifyResponse.status);
230
+ throw new Error('Failed to verify authentication. Session may not be created.');
231
+ }
232
+
233
+ userData = await verifyResponse.json();
182
234
  }
183
235
 
184
- const userData = await verifyResponse.json();
185
236
  console.log('[X OAuth] Authentication verified:', userData);
186
237
 
187
238
  // Send success to opener
188
239
  if (window.opener) {
189
- window.opener.postMessage({
240
+ const message = {
190
241
  type: 'X_AUTH_SUCCESS',
191
242
  provider: 'x',
192
243
  user: userData,
193
244
  mode: MODE
194
- }, '*');
245
+ };
246
+
247
+ // Include tokens if we have them (new flow)
248
+ if (tokens) {
249
+ message.tokens = tokens;
250
+ }
251
+
252
+ window.opener.postMessage(message, '*');
195
253
 
196
254
  // Mark that we've sent the auth result
197
255
  authResultSent = true;
package/dist/index.cjs CHANGED
@@ -703,52 +703,72 @@ async function ensureDkgAndGetOwner(userId, _clientSeedHex) {
703
703
  throw error;
704
704
  }
705
705
  }
706
+ function isChannelError(error) {
707
+ const message = error.message.toLowerCase();
708
+ return message.includes("invalid sdk channel") || message.includes("sdk channel not found") || message.includes("sdk channel expired") || // Backward compatibility
709
+ message === "invalid session";
710
+ }
706
711
  async function signDigestWithMpc(userId, digest32, userOpDetails) {
712
+ const MAX_RETRIES = 1;
707
713
  const startTime = performance.now();
708
714
  currentSigningStats = {
709
715
  startTime,
710
716
  rounds: []
711
717
  };
712
- try {
713
- const iframeManager = getIframeManager();
714
- const { jwtTokenManager: jwtTokenManager4 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
715
- const accessToken = jwtTokenManager4.getAccessToken();
716
- if (!accessToken) {
717
- throw new Error("No access token available for signing");
718
- }
719
- const transaction = {
720
- to: userOpDetails?.callTarget || "0x0000000000000000000000000000000000000000",
721
- value: userOpDetails?.value || "0",
722
- data: userOpDetails?.callData || "0x",
723
- digest32,
724
- // Pre-computed digest - DO NOT recompute!
725
- // Additional UserOp fields for display in confirmation modal
726
- userOpDetails
727
- };
728
- const signature = await iframeManager.signTransaction(userId, transaction, accessToken);
729
- const endTime = performance.now();
730
- currentSigningStats.endTime = endTime;
731
- currentSigningStats.totalDurationMs = endTime - startTime;
732
- return signature;
733
- } catch (error) {
734
- (0, import_error_tracking.logSdkError)(
735
- error instanceof Error ? error : new Error("MPC signing failed"),
736
- { userId, hasUserOpDetails: !!userOpDetails },
737
- "iframe-mpc"
738
- );
739
- const endTime = performance.now();
740
- if (currentSigningStats) {
718
+ let lastError;
719
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
720
+ try {
721
+ const iframeManager = getIframeManager();
722
+ const { jwtTokenManager: jwtTokenManager4 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
723
+ const accessToken = jwtTokenManager4.getAccessToken();
724
+ if (!accessToken) {
725
+ throw new Error("No access token available for signing");
726
+ }
727
+ const transaction = {
728
+ to: userOpDetails?.callTarget || "0x0000000000000000000000000000000000000000",
729
+ value: userOpDetails?.value || "0",
730
+ data: userOpDetails?.callData || "0x",
731
+ digest32,
732
+ // Pre-computed digest - DO NOT recompute!
733
+ // Additional UserOp fields for display in confirmation modal
734
+ userOpDetails
735
+ };
736
+ const signature = await iframeManager.signTransaction(userId, transaction, accessToken);
737
+ const endTime = performance.now();
741
738
  currentSigningStats.endTime = endTime;
742
739
  currentSigningStats.totalDurationMs = endTime - startTime;
740
+ return signature;
741
+ } catch (error) {
742
+ lastError = error instanceof Error ? error : new Error("MPC signing failed");
743
+ if (isChannelError(lastError) && attempt < MAX_RETRIES) {
744
+ console.warn(`[MPC] SDK channel error, reconnecting and retrying (attempt ${attempt + 1})...`);
745
+ try {
746
+ await getIframeManager().reconnect();
747
+ continue;
748
+ } catch (reconnectError) {
749
+ console.error("[MPC] Reconnect failed:", reconnectError);
750
+ }
751
+ }
752
+ (0, import_error_tracking.logSdkError)(
753
+ lastError,
754
+ { userId, hasUserOpDetails: !!userOpDetails, attempt },
755
+ "iframe-mpc"
756
+ );
757
+ const endTime = performance.now();
758
+ if (currentSigningStats) {
759
+ currentSigningStats.endTime = endTime;
760
+ currentSigningStats.totalDurationMs = endTime - startTime;
761
+ }
762
+ if (lastError instanceof LumiaPassportError) {
763
+ throw lastError;
764
+ }
765
+ throw new LumiaPassportError(
766
+ lastError.message,
767
+ ErrorCodes.MPC_SIGNING_ERROR
768
+ );
743
769
  }
744
- if (error instanceof LumiaPassportError) {
745
- throw error;
746
- }
747
- throw new LumiaPassportError(
748
- error instanceof Error ? error.message : "MPC signing failed",
749
- ErrorCodes.MPC_SIGNING_ERROR
750
- );
751
770
  }
771
+ throw lastError || new Error("MPC signing failed after retries");
752
772
  }
753
773
  async function signTypedDataWithMpc(userId, digest32, typedData) {
754
774
  const startTime = performance.now();
@@ -1379,7 +1399,7 @@ async function getShareVaultToken(scopes) {
1379
1399
  }
1380
1400
  return {
1381
1401
  token: data.resourceToken,
1382
- expiresAt: Date.now() + data.expiresIn * 1e3
1402
+ expiresAt: Date.now() + (data.expiresIn || 300) * 1e3
1383
1403
  };
1384
1404
  }
1385
1405
  async function getShareRecoveryStats() {
@@ -2620,10 +2640,9 @@ var init_iframe_manager = __esm({
2620
2640
  "src/internal/lib/iframe-manager.ts"() {
2621
2641
  init_errors();
2622
2642
  IframeManager = class {
2623
- // 5 minutes
2624
2643
  constructor(config) {
2625
2644
  this.iframe = null;
2626
- this.sessionToken = null;
2645
+ this.channelToken = null;
2627
2646
  this.isReady = false;
2628
2647
  // Message handling
2629
2648
  this.pendingRequests = /* @__PURE__ */ new Map();
@@ -2640,6 +2659,9 @@ var init_iframe_manager = __esm({
2640
2659
  this.REQUEST_TIMEOUT = 3e5;
2641
2660
  // 5 minutes (for user interactions like consent)
2642
2661
  this.NONCE_EXPIRY = 3e5;
2662
+ // 5 minutes
2663
+ this.HEARTBEAT_INTERVAL = 5 * 60 * 1e3;
2664
+ this.isReconnecting = false;
2643
2665
  this.iframeUrl = config.iframeUrl;
2644
2666
  this.projectId = config.projectId;
2645
2667
  this.debug = config.debug || false;
@@ -2680,6 +2702,8 @@ var init_iframe_manager = __esm({
2680
2702
  await this.readyPromise;
2681
2703
  await this.authenticateSDK();
2682
2704
  await this.primeProviderSessions();
2705
+ this.startHeartbeat();
2706
+ this.setupVisibilityHandler();
2683
2707
  this.log("[IframeManager] \u2705 Iframe ready and authenticated");
2684
2708
  }
2685
2709
  /**
@@ -2689,18 +2713,93 @@ var init_iframe_manager = __esm({
2689
2713
  this.onWalletReadyCallback = callback;
2690
2714
  }
2691
2715
  /**
2692
- * Authenticate SDK with iframe to establish session
2716
+ * Authenticate SDK with iframe to establish secure channel
2693
2717
  */
2694
2718
  async authenticateSDK() {
2695
2719
  const response = await this.sendMessage("SDK_AUTH", {
2696
2720
  projectId: this.projectId
2697
2721
  });
2698
2722
  if (response.type === "LUMIA_PASSPORT_SDK_AUTH_SUCCESS") {
2699
- this.sessionToken = response.sessionToken;
2723
+ this.channelToken = response.sessionToken;
2700
2724
  } else {
2701
- throw new Error("SDK authentication failed");
2725
+ throw new Error("SDK channel authentication failed");
2702
2726
  }
2703
2727
  }
2728
+ /**
2729
+ * Start periodic heartbeat to check SDK channel validity
2730
+ */
2731
+ startHeartbeat() {
2732
+ if (this.heartbeatInterval) {
2733
+ clearInterval(this.heartbeatInterval);
2734
+ }
2735
+ this.heartbeatInterval = setInterval(async () => {
2736
+ if (!this.channelToken) return;
2737
+ try {
2738
+ const response = await this.sendMessage("SDK_CHANNEL_HEARTBEAT", {});
2739
+ if (!response.valid) {
2740
+ this.log("[IframeManager] SDK channel invalid, reconnecting...");
2741
+ await this.reconnect();
2742
+ }
2743
+ } catch (error) {
2744
+ this.log("[IframeManager] Heartbeat failed:", error);
2745
+ await this.reconnect();
2746
+ }
2747
+ }, this.HEARTBEAT_INTERVAL);
2748
+ this.log("[IframeManager] Heartbeat started (interval: 5 min)");
2749
+ }
2750
+ /**
2751
+ * Stop heartbeat
2752
+ */
2753
+ stopHeartbeat() {
2754
+ if (this.heartbeatInterval) {
2755
+ clearInterval(this.heartbeatInterval);
2756
+ this.heartbeatInterval = void 0;
2757
+ this.log("[IframeManager] Heartbeat stopped");
2758
+ }
2759
+ }
2760
+ /**
2761
+ * Reconnect SDK channel after it becomes invalid
2762
+ */
2763
+ async reconnect() {
2764
+ if (this.isReconnecting) {
2765
+ this.log("[IframeManager] Already reconnecting, skipping...");
2766
+ return;
2767
+ }
2768
+ this.isReconnecting = true;
2769
+ this.log("[IframeManager] Reconnecting SDK channel...");
2770
+ try {
2771
+ this.channelToken = null;
2772
+ await this.authenticateSDK();
2773
+ this.log("[IframeManager] \u2705 SDK channel reconnected");
2774
+ } catch (error) {
2775
+ this.log("[IframeManager] \u274C Reconnect failed:", error);
2776
+ throw error;
2777
+ } finally {
2778
+ this.isReconnecting = false;
2779
+ }
2780
+ }
2781
+ /**
2782
+ * Setup visibility change handler to check channel when tab becomes visible
2783
+ */
2784
+ setupVisibilityHandler() {
2785
+ if (typeof document === "undefined") return;
2786
+ document.addEventListener("visibilitychange", async () => {
2787
+ if (document.visibilityState === "visible" && this.channelToken) {
2788
+ this.log("[IframeManager] Tab visible, checking SDK channel...");
2789
+ try {
2790
+ const response = await this.sendMessage("SDK_CHANNEL_HEARTBEAT", {});
2791
+ if (!response.valid) {
2792
+ this.log("[IframeManager] SDK channel expired while tab was hidden");
2793
+ await this.reconnect();
2794
+ }
2795
+ } catch (error) {
2796
+ this.log("[IframeManager] Channel check failed, reconnecting...");
2797
+ await this.reconnect();
2798
+ }
2799
+ }
2800
+ });
2801
+ this.log("[IframeManager] Visibility handler setup");
2802
+ }
2704
2803
  /**
2705
2804
  * Handle incoming postMessage events
2706
2805
  */
@@ -2724,6 +2823,7 @@ var init_iframe_manager = __esm({
2724
2823
  "LUMIA_PASSPORT_TRUSTED_APP_REMOVED",
2725
2824
  "LUMIA_PASSPORT_REQUEST_NEW_TOKEN",
2726
2825
  "LUMIA_PASSPORT_TOKEN_REFRESHED",
2826
+ "LUMIA_PASSPORT_HEARTBEAT_RESPONSE",
2727
2827
  "LUMIA_PASSPORT_RESPONSE",
2728
2828
  "LUMIA_PASSPORT_ERROR"
2729
2829
  ];
@@ -2852,10 +2952,11 @@ var init_iframe_manager = __esm({
2852
2952
  projectId: this.projectId,
2853
2953
  data: {
2854
2954
  ...data,
2855
- sessionToken: this.sessionToken
2955
+ sessionToken: this.channelToken
2956
+ // named sessionToken for backward compatibility with iframe
2856
2957
  }
2857
2958
  };
2858
- if (this.sessionToken) {
2959
+ if (this.channelToken) {
2859
2960
  message.hmac = await this.computeHMAC(message);
2860
2961
  }
2861
2962
  const responsePromise = new Promise((resolve, reject) => {
@@ -2877,7 +2978,7 @@ var init_iframe_manager = __esm({
2877
2978
  return responsePromise;
2878
2979
  }
2879
2980
  /**
2880
- * Compute HMAC for message authentication
2981
+ * Compute HMAC for message authentication using SDK channel token
2881
2982
  */
2882
2983
  async computeHMAC(message) {
2883
2984
  const encoder = new TextEncoder();
@@ -2889,7 +2990,7 @@ var init_iframe_manager = __esm({
2889
2990
  data: JSON.stringify(message.data)
2890
2991
  });
2891
2992
  const data = encoder.encode(payload);
2892
- const key = encoder.encode(this.sessionToken);
2993
+ const key = encoder.encode(this.channelToken);
2893
2994
  const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
2894
2995
  const signature = await crypto.subtle.sign("HMAC", cryptoKey, data);
2895
2996
  return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
@@ -3279,6 +3380,24 @@ var init_iframe_manager = __esm({
3279
3380
  });
3280
3381
  } else if (event.data.type === "X_AUTH_SUCCESS" && eventProvider === "x" && providerKey === "x") {
3281
3382
  this.log("[IframeManager] X auth successful from popup:", event.data);
3383
+ if (event.data.tokens) {
3384
+ this.log("[IframeManager] Tokens received in postMessage, storing via jwtTokenManager");
3385
+ Promise.resolve().then(() => (init_auth(), auth_exports)).then(({ jwtTokenManager: jwtTokenManager4 }) => {
3386
+ return jwtTokenManager4.setTokens({
3387
+ accessToken: event.data.tokens.accessToken,
3388
+ refreshToken: event.data.tokens.refreshToken,
3389
+ userId: event.data.user.userId,
3390
+ expiresIn: event.data.user.expiresIn || 3600,
3391
+ hasKeyshare: event.data.user.hasKeyshare || false,
3392
+ displayName: event.data.user.displayName || null,
3393
+ providers: event.data.user.providers || ["x"]
3394
+ });
3395
+ }).then(() => {
3396
+ this.log("[IframeManager] Tokens stored successfully");
3397
+ }).catch((tokenError) => {
3398
+ this.log("[IframeManager] Warning: Failed to store tokens:", tokenError);
3399
+ });
3400
+ }
3282
3401
  finalize({
3283
3402
  success: true,
3284
3403
  user: event.data.user,
@@ -3634,6 +3753,7 @@ var init_iframe_manager = __esm({
3634
3753
  */
3635
3754
  destroy() {
3636
3755
  this.log("[IframeManager] Destroying iframe...");
3756
+ this.stopHeartbeat();
3637
3757
  this.pendingRequests.forEach((pending) => {
3638
3758
  pending.reject(new Error("Iframe manager destroyed"));
3639
3759
  });
@@ -3647,7 +3767,7 @@ var init_iframe_manager = __esm({
3647
3767
  }
3648
3768
  this.iframe = null;
3649
3769
  this.isReady = false;
3650
- this.sessionToken = null;
3770
+ this.channelToken = null;
3651
3771
  this.log("[IframeManager] \u2705 Destroyed");
3652
3772
  }
3653
3773
  /**
@@ -5570,7 +5690,7 @@ function Header() {
5570
5690
  // package.json
5571
5691
  var package_default = {
5572
5692
  name: "@lumiapassport/ui-kit",
5573
- version: "1.16.0",
5693
+ version: "1.16.2",
5574
5694
  description: "React UI components and hooks for Lumia Passport authentication and Account Abstraction",
5575
5695
  type: "module",
5576
5696
  main: "./dist/index.cjs",
@@ -9991,6 +10111,7 @@ var useManageWalletStore = (0, import_zustand5.create)((set) => ({
9991
10111
  emailCode: "",
9992
10112
  emailCodeExpiresIn: 0,
9993
10113
  linkIsLoading: false,
10114
+ linkError: null,
9994
10115
  providerType: null,
9995
10116
  confirmUnlink: null,
9996
10117
  setAlert: (alert2) => set({ alert: alert2 }),
@@ -9999,6 +10120,7 @@ var useManageWalletStore = (0, import_zustand5.create)((set) => ({
9999
10120
  setEmailCode: (emailCode) => set({ emailCode }),
10000
10121
  setEmailCodeExpiresIn: (emailCodeExpiresIn) => set({ emailCodeExpiresIn }),
10001
10122
  setLinkIsLoading: (linkIsLoading) => set({ linkIsLoading }),
10123
+ setLinkError: (linkError) => set({ linkError }),
10002
10124
  setProviderType: (providerType) => set({ providerType }),
10003
10125
  setConfirmUnlink: (confirmUnlink) => set({ confirmUnlink })
10004
10126
  }));