@glideidentity/web-client-sdk 4.4.8-beta.3 → 4.4.8

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.
@@ -17,8 +17,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.LinkHandler = void 0;
18
18
  class LinkHandler {
19
19
  constructor() {
20
- this.isPolling = false;
20
+ this.isPollingActive = false;
21
21
  this.isCancelled = false;
22
+ this.isPollingInProgress = false;
22
23
  }
23
24
  /**
24
25
  * Invoke link-based authentication
@@ -26,7 +27,6 @@ class LinkHandler {
26
27
  */
27
28
  invoke(data, options) {
28
29
  return __awaiter(this, void 0, void 0, function* () {
29
- console.log('[Link Auth] 🔗 SDK Version: 4.4.8-beta.2 (with enhanced error logging)');
30
30
  console.log('[Link Auth] 🔗 invoke() called with data:', JSON.stringify(data, null, 2));
31
31
  console.log('[Link Auth] Options:', options ? JSON.stringify({
32
32
  pollingInterval: options.pollingInterval,
@@ -70,7 +70,7 @@ class LinkHandler {
70
70
  startPolling(sessionKey, linkData, options) {
71
71
  return __awaiter(this, void 0, void 0, function* () {
72
72
  const interval = (options === null || options === void 0 ? void 0 : options.pollingInterval) || 2000; // Fixed 2 second interval
73
- const maxAttempts = (options === null || options === void 0 ? void 0 : options.maxPollingAttempts) || 150; // 5 minutes with 2s interval
73
+ const maxAttempts = (options === null || options === void 0 ? void 0 : options.maxPollingAttempts) || 30; // 1 minute with 2s interval
74
74
  let attempts = 0;
75
75
  console.log('[Link Auth] 🚀 Starting polling:', {
76
76
  sessionKey,
@@ -79,27 +79,37 @@ class LinkHandler {
79
79
  linkDataAvailable: !!linkData
80
80
  });
81
81
  return new Promise((resolve, reject) => {
82
- this.isPolling = true;
82
+ this.isPollingActive = true;
83
+ this.pollingReject = reject; // Store reject function for cancel()
83
84
  const poll = () => __awaiter(this, void 0, void 0, function* () {
84
- if (!this.isPolling) {
85
+ if (!this.isPollingActive) {
85
86
  return; // Polling was stopped
86
87
  }
88
+ // Skip if another poll is already in progress
89
+ if (this.isPollingInProgress) {
90
+ return;
91
+ }
87
92
  let statusUrl = ''; // Declare at function scope for catch block access
88
93
  try {
89
- attempts++;
90
- // Check max attempts
94
+ this.isPollingInProgress = true;
95
+ // Check max attempts before making the request
91
96
  if (attempts >= maxAttempts) {
92
97
  this.stopPolling();
93
98
  if (options === null || options === void 0 ? void 0 : options.onTimeout) {
94
99
  options.onTimeout();
95
100
  }
101
+ // Calculate actual timeout duration
102
+ const timeoutSeconds = Math.round((maxAttempts * interval) / 1000);
103
+ const timeoutMessage = timeoutSeconds >= 60
104
+ ? `${Math.floor(timeoutSeconds / 60)} minute${Math.floor(timeoutSeconds / 60) > 1 ? 's' : ''}`
105
+ : `${timeoutSeconds} seconds`;
96
106
  if (options === null || options === void 0 ? void 0 : options.onStatusUpdate) {
97
107
  options.onStatusUpdate({
98
108
  status: 'expired',
99
- message: 'Authentication timeout'
109
+ message: `Authentication timeout after ${timeoutMessage}`
100
110
  });
101
111
  }
102
- reject(new Error('Authentication timeout after 5 minutes'));
112
+ reject(new Error(`Authentication timeout after ${timeoutMessage}`));
103
113
  return;
104
114
  }
105
115
  // Build public status endpoint URL
@@ -178,6 +188,7 @@ class LinkHandler {
178
188
  });
179
189
  }
180
190
  // Return the authentication result
191
+ this.pollingReject = undefined; // Clear reject function on success
181
192
  resolve({
182
193
  authenticated: true,
183
194
  credential: result.credential || sessionKey,
@@ -193,6 +204,7 @@ class LinkHandler {
193
204
  else if (result.status === 'pending') {
194
205
  // Continue polling
195
206
  console.log('[Link Auth] Status still pending, continuing to poll...');
207
+ attempts++; // Increment attempts after successful poll
196
208
  if (options === null || options === void 0 ? void 0 : options.onStatusUpdate) {
197
209
  options.onStatusUpdate({
198
210
  status: 'pending',
@@ -203,6 +215,7 @@ class LinkHandler {
203
215
  else {
204
216
  // Unexpected status value
205
217
  console.log('[Link Auth] ⚠️ Unexpected status value:', result.status, 'Full result:', JSON.stringify(result, null, 2));
218
+ attempts++; // Increment for unexpected status too
206
219
  }
207
220
  }
208
221
  else if (response.status === 410) {
@@ -263,6 +276,7 @@ class LinkHandler {
263
276
  else {
264
277
  // Unexpected status - continue polling
265
278
  console.log('[Link Auth] ⚠️ Unexpected HTTP status:', response.status, 'continuing to poll...');
279
+ attempts++; // Increment for unexpected HTTP status
266
280
  try {
267
281
  const body = yield response.text();
268
282
  console.log('[Link Auth] Response body:', body);
@@ -275,6 +289,7 @@ class LinkHandler {
275
289
  catch (error) {
276
290
  // Network or other error - continue polling
277
291
  console.error('[Link Auth] 🔴 Polling error:', error.message || error);
292
+ attempts++; // Increment for error case
278
293
  console.error('[Link Auth] Error details:', {
279
294
  name: error.name,
280
295
  message: error.message,
@@ -295,6 +310,10 @@ class LinkHandler {
295
310
  });
296
311
  }
297
312
  }
313
+ finally {
314
+ // Always clear the polling flag when done
315
+ this.isPollingInProgress = false;
316
+ }
298
317
  });
299
318
  // Start initial poll immediately
300
319
  poll();
@@ -308,7 +327,8 @@ class LinkHandler {
308
327
  */
309
328
  stopPolling() {
310
329
  console.log('[Link Auth] 🏁 Stopping polling');
311
- this.isPolling = false;
330
+ this.isPollingActive = false;
331
+ this.isPollingInProgress = false;
312
332
  if (this.pollingInterval) {
313
333
  clearInterval(this.pollingInterval);
314
334
  this.pollingInterval = undefined;
@@ -343,15 +363,31 @@ class LinkHandler {
343
363
  this.stopPolling();
344
364
  this.isCancelled = false;
345
365
  this.onCancel = undefined;
366
+ this.pollingReject = undefined;
367
+ }
368
+ /**
369
+ * Check if polling is currently active
370
+ */
371
+ isPolling() {
372
+ return this.pollingInterval !== undefined;
346
373
  }
347
374
  /**
348
375
  * Cancel the ongoing authentication
349
376
  */
350
377
  cancel() {
351
378
  var _a;
379
+ console.log('[Link Auth] Cancelling authentication');
352
380
  this.isCancelled = true;
353
381
  this.stopPolling();
354
382
  (_a = this.onCancel) === null || _a === void 0 ? void 0 : _a.call(this);
383
+ // Immediately reject the polling promise
384
+ if (this.pollingReject) {
385
+ this.pollingReject({
386
+ code: 'USER_DENIED',
387
+ message: 'Authentication cancelled by user'
388
+ });
389
+ this.pollingReject = undefined;
390
+ }
355
391
  }
356
392
  }
357
393
  exports.LinkHandler = LinkHandler;
@@ -72,7 +72,7 @@ export interface AuthConfig extends PhoneAuthCallbacks {
72
72
  pollingInterval?: number;
73
73
  /**
74
74
  * Maximum polling attempts before timeout
75
- * @default 150 (5 minutes with 2s interval)
75
+ * @default 30 (1 minute with 2s interval)
76
76
  */
77
77
  maxPollingAttempts?: number;
78
78
  /**
@@ -26,6 +26,10 @@ export declare class AuthModal {
26
26
  private closeCallback?;
27
27
  constructor(options?: InvokeOptions['modalOptions'], callbacks?: InvokeOptions['callbacks']);
28
28
  private handleEscapeKey;
29
+ /**
30
+ * Escape HTML to prevent XSS attacks
31
+ */
32
+ private escapeHtml;
29
33
  /**
30
34
  * Shows the modal with a QR code for desktop authentication
31
35
  * Supports both single QR code (legacy) and dual-platform QR codes (iOS + Android)
@@ -47,6 +47,17 @@ class AuthModal {
47
47
  }
48
48
  }
49
49
  }
50
+ /**
51
+ * Escape HTML to prevent XSS attacks
52
+ */
53
+ escapeHtml(unsafe) {
54
+ return unsafe
55
+ .replace(/&/g, "&")
56
+ .replace(/</g, "&lt;")
57
+ .replace(/>/g, "&gt;")
58
+ .replace(/"/g, "&quot;")
59
+ .replace(/'/g, "&#039;");
60
+ }
50
61
  /**
51
62
  * Shows the modal with a QR code for desktop authentication
52
63
  * Supports both single QR code (legacy) and dual-platform QR codes (iOS + Android)
@@ -75,7 +86,7 @@ class AuthModal {
75
86
  // Only iOS QR code available - show single QR
76
87
  this.createModal(`
77
88
  <div class="glide-auth-qr-container">
78
- <img src="${qrCodeData.iosQRCode}" alt="QR Code" class="glide-auth-qr-code" />
89
+ <img src="${this.escapeHtml(qrCodeData.iosQRCode)}" alt="QR Code" class="glide-auth-qr-code" />
79
90
  <p class="glide-auth-status">Scan with your iPhone to authenticate</p>
80
91
  </div>
81
92
  `);
@@ -86,8 +97,8 @@ class AuthModal {
86
97
  // Legacy single QR code
87
98
  this.createModal(`
88
99
  <div class="glide-auth-qr-container">
89
- <img src="${qrCodeData}" alt="QR Code" class="glide-auth-qr-code" />
90
- <p class="glide-auth-status">${statusMessage}</p>
100
+ <img src="${this.escapeHtml(qrCodeData)}" alt="QR Code" class="glide-auth-qr-code" />
101
+ <p class="glide-auth-status">${this.escapeHtml(statusMessage)}</p>
91
102
  </div>
92
103
  `);
93
104
  }
@@ -118,11 +129,11 @@ class AuthModal {
118
129
  <!-- QR Code Image -->
119
130
  <img
120
131
  id="glide-qr-code-img"
121
- src="${qrCodeData.iosQRCode}"
132
+ src="${this.escapeHtml(qrCodeData.iosQRCode)}"
122
133
  alt="QR Code"
123
134
  class="glide-auth-qr-code"
124
- data-ios="${qrCodeData.iosQRCode}"
125
- data-android="${qrCodeData.androidQRCode}"
135
+ data-ios="${this.escapeHtml(qrCodeData.iosQRCode)}"
136
+ data-android="${this.escapeHtml(qrCodeData.androidQRCode || '')}"
126
137
  />
127
138
 
128
139
  <!-- Status Message -->
@@ -2,4 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SDK_VERSION = void 0;
4
4
  // SDK version - injected at build time
5
- exports.SDK_VERSION = '4.4.8-beta.3';
5
+ exports.SDK_VERSION = '4.4.8';
@@ -170,6 +170,10 @@ export class PhoneAuthClient {
170
170
  }
171
171
  // Step 3: Process the response through appropriate endpoint
172
172
  const credential = credentialResponse;
173
+ // Validate use_case is provided for endpoint selection
174
+ if (!options.use_case) {
175
+ throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'use_case is required', { field: 'use_case' });
176
+ }
173
177
  const result = options.use_case === API.USE_CASE.GET_PHONE_NUMBER
174
178
  ? yield this.getPhoneNumber(credential, preparedRequest.session)
175
179
  : yield this.verifyPhoneNumber(credential, preparedRequest.session);
@@ -278,16 +282,7 @@ export class PhoneAuthClient {
278
282
  */
279
283
  preparePhoneRequest(options) {
280
284
  return __awaiter(this, void 0, void 0, function* () {
281
- // Validate use case requirements first
282
- // const useCaseValidation = validateUseCaseRequirements(options.use_case, options.phone_number);
283
- // if (!useCaseValidation.valid) {
284
- // throw this.createError(
285
- // PhoneAuthErrorCode.BAD_REQUEST,
286
- // useCaseValidation.error!,
287
- // { field: 'use_case' }
288
- // );
289
- // }
290
- var _a, _b;
285
+ var _a, _b, _c;
291
286
  // Validate phone number if provided
292
287
  if (options.phone_number) {
293
288
  const phoneValidation = validatePhoneNumber(options.phone_number);
@@ -309,8 +304,12 @@ export class PhoneAuthClient {
309
304
  throw this.createError(PhoneAuthErrorCode.BAD_REQUEST, consentValidation.error, { field: 'consent_data' });
310
305
  }
311
306
  }
307
+ // Validate use_case is provided (unless only parent_session_id is given)
308
+ if (!options.use_case && !(((_a = options.options) === null || _a === void 0 ? void 0 : _a.parent_session_id) && !options.phone_number && !options.plmn)) {
309
+ throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'use_case is required', { field: 'use_case' });
310
+ }
312
311
  // Validate required parameters based on use case
313
- if (!options.phone_number && !options.plmn && !((_a = options.options) === null || _a === void 0 ? void 0 : _a.parent_session_id)) {
312
+ if (!options.phone_number && !options.plmn && !((_b = options.options) === null || _b === void 0 ? void 0 : _b.parent_session_id)) {
314
313
  // Provide specific error message based on use case
315
314
  if (options.use_case === API.USE_CASE.GET_PHONE_NUMBER) {
316
315
  throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'PLMN (MCC/MNC) is required for GetPhoneNumber. Please provide carrier network information.', { field: 'plmn', useCase: 'GetPhoneNumber' });
@@ -318,19 +317,15 @@ export class PhoneAuthClient {
318
317
  else if (options.use_case === API.USE_CASE.VERIFY_PHONE_NUMBER) {
319
318
  throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'Phone number is required for VerifyPhoneNumber', { field: 'phoneNumber', useCase: 'VerifyPhoneNumber' });
320
319
  }
321
- else if (!options.use_case) {
322
- // If no use case, that's an error
323
- throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'use_case is required', { field: 'use_case' });
324
- }
325
320
  else {
326
321
  // Fallback for other use cases
327
322
  throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'Either phone number or PLMN (MCC/MNC) must be provided', { field: 'phoneNumber,plmn' });
328
323
  }
329
324
  }
330
- // If only parent_session_id is provided, use_case is optional (will be inherited)
331
- if (((_b = options.options) === null || _b === void 0 ? void 0 : _b.parent_session_id) && !options.phone_number && !options.plmn && !options.use_case) {
325
+ // Log parent session usage
326
+ if (((_c = options.options) === null || _c === void 0 ? void 0 : _c.parent_session_id) && !options.phone_number && !options.plmn) {
332
327
  if (this.debug) {
333
- console.log('[PhoneAuth] Using parent_session_id only - all fields will be inherited from parent session');
328
+ console.log('[PhoneAuth] Using parent_session_id: %s, use_case: %s', options.options.parent_session_id, options.use_case || 'not provided');
334
329
  }
335
330
  }
336
331
  const requestId = `web-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
@@ -345,11 +340,13 @@ export class PhoneAuthClient {
345
340
  throw this.createError(PhoneAuthErrorCode.INTERNAL_SERVER_ERROR, 'Failed to generate valid nonce', { field: 'nonce' });
346
341
  }
347
342
  // Build properly typed request body according to API specification
343
+ // Be permissive - backend will ignore extra fields if not needed
348
344
  const requestBody = {
349
- // Required fields
345
+ // Include use_case if provided (optional when parent_session_id is given)
350
346
  use_case: options.use_case,
351
- // One of these is required
347
+ // Include phone_number if provided (backend ignores if not needed)
352
348
  phone_number: options.phone_number,
349
+ // Include PLMN if provided
353
350
  plmn: options.plmn ? {
354
351
  mcc: options.plmn.mcc,
355
352
  mnc: options.plmn.mnc
@@ -381,7 +378,7 @@ export class PhoneAuthClient {
381
378
  // Always include the HTTP status from the response
382
379
  errorDetails = Object.assign(Object.assign({}, errorDetails), { status: response.status });
383
380
  }
384
- catch (_c) {
381
+ catch (_d) {
385
382
  // If JSON parsing fails, use status text
386
383
  errorDetails = { status: response.status, statusText: response.statusText };
387
384
  }
@@ -649,12 +646,15 @@ export class PhoneAuthClient {
649
646
  });
650
647
  // TS43 always auto-triggers (no SDK UI ever)
651
648
  // The Digital Credentials API provides its own OS-level UI
652
- let credentialPromise;
649
+ // Use a wrapper object to allow updating the promise reference
650
+ const credentialWrapper = {
651
+ promise: null
652
+ };
653
653
  try {
654
654
  // Always try to trigger immediately
655
655
  const vpToken = yield enhancedTriggerTS43();
656
656
  // Convert to AuthCredential format
657
- credentialPromise = Promise.resolve({
657
+ credentialWrapper.promise = Promise.resolve({
658
658
  credential: typeof vpToken === 'string' ? vpToken : Object.values(vpToken)[0],
659
659
  session: plainResponse.session,
660
660
  authenticated: true
@@ -662,36 +662,46 @@ export class PhoneAuthClient {
662
662
  }
663
663
  catch (error) {
664
664
  // If auto-trigger fails, create a rejected promise
665
- credentialPromise = Promise.reject(error);
665
+ credentialWrapper.promise = Promise.reject(error);
666
666
  }
667
667
  // Handle based on execution mode
668
668
  if (executionMode === 'extended') {
669
669
  // Extended mode - return control methods
670
- return {
670
+ const response = {
671
671
  strategy: 'ts43',
672
672
  session: plainResponse.session,
673
- credential: credentialPromise,
673
+ credential: credentialWrapper.promise, // Initial value
674
674
  // Re-trigger credential request
675
675
  trigger: () => __awaiter(this, void 0, void 0, function* () {
676
676
  const vpToken = yield enhancedTriggerTS43();
677
- // Update the credential promise
678
- credentialPromise = Promise.resolve({
677
+ // Update the credential promise in the wrapper
678
+ credentialWrapper.promise = Promise.resolve({
679
679
  credential: typeof vpToken === 'string' ? vpToken : Object.values(vpToken)[0],
680
680
  session: plainResponse.session,
681
681
  authenticated: true
682
682
  });
683
+ // Return void as per interface
683
684
  }),
684
685
  cancel: () => {
685
686
  // TS43 doesn't have a way to cancel once triggered
686
687
  // but we can reject the promise
687
- credentialPromise = Promise.reject(this.createError(PhoneAuthErrorCode.USER_DENIED, 'Authentication cancelled'));
688
+ credentialWrapper.promise = Promise.reject(this.createError(PhoneAuthErrorCode.USER_DENIED, 'Authentication cancelled'));
688
689
  }
689
690
  };
691
+ // Define credential as a getter that always returns the current promise
692
+ Object.defineProperty(response, 'credential', {
693
+ get() {
694
+ return credentialWrapper.promise;
695
+ },
696
+ enumerable: true,
697
+ configurable: true
698
+ });
699
+ return response;
690
700
  }
691
701
  else {
692
702
  // Standard mode - just return credential
693
703
  // Wait for and return the credential
694
- const credential = yield credentialPromise;
704
+ const credential = yield credentialWrapper.promise;
695
705
  // Return in standard format
696
706
  return {
697
707
  [plainResponse.session.session_key]: credential.credential
@@ -721,7 +731,7 @@ export class PhoneAuthClient {
721
731
  const pollingEndpointToUse = (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.pollingEndpoint) ||
722
732
  (desktopOptions === null || desktopOptions === void 0 ? void 0 : desktopOptions.pollingEndpoint) ||
723
733
  ((_m = this.config.endpoints) === null || _m === void 0 ? void 0 : _m.polling);
724
- const pollingOptions = Object.assign(Object.assign({}, desktopOptions), { pollingEndpoint: pollingEndpointToUse, pollingInterval: (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.pollingInterval) || (desktopOptions === null || desktopOptions === void 0 ? void 0 : desktopOptions.pollingInterval) || this.config.pollingInterval || 2000, maxPollingAttempts: (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.maxPollingAttempts) || (desktopOptions === null || desktopOptions === void 0 ? void 0 : desktopOptions.maxPollingAttempts) || this.config.maxPollingAttempts || 150, onQRCodeReady: undefined, onStatusUpdate: undefined });
734
+ const pollingOptions = Object.assign(Object.assign({}, desktopOptions), { pollingEndpoint: pollingEndpointToUse, pollingInterval: (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.pollingInterval) || (desktopOptions === null || desktopOptions === void 0 ? void 0 : desktopOptions.pollingInterval) || this.config.pollingInterval || 2000, maxPollingAttempts: (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.maxPollingAttempts) || (desktopOptions === null || desktopOptions === void 0 ? void 0 : desktopOptions.maxPollingAttempts) || this.config.maxPollingAttempts || 30, onQRCodeReady: undefined, onStatusUpdate: undefined });
725
735
  // Decide whether to show modal based on preventDefaultUI
726
736
  const showModal = !preventDefaultUI;
727
737
  let modal;
@@ -784,31 +794,79 @@ export class PhoneAuthClient {
784
794
  // Handle based on execution mode
785
795
  if (executionMode === 'extended') {
786
796
  // Extended mode - return control methods
787
- const credentialPromise = showModal ? startPolling() : Promise.resolve({
788
- credential: '',
789
- session: plainResponse.session,
790
- authenticated: false
797
+ let credentialResolve = null;
798
+ let credentialReject = null;
799
+ let pollingStarted = false;
800
+ // Create a promise that will remain pending until polling starts
801
+ const credentialPromise = new Promise((resolve, reject) => {
802
+ credentialResolve = resolve;
803
+ credentialReject = reject;
804
+ // If modal is shown, start polling immediately
805
+ if (showModal) {
806
+ pollingStarted = true;
807
+ startPolling()
808
+ .then(resolve)
809
+ .catch(reject);
810
+ }
811
+ // Otherwise, promise remains pending until start_polling() is called
791
812
  });
792
- return {
813
+ // Create wrapped functions
814
+ const wrappedStartPolling = () => __awaiter(this, void 0, void 0, function* () {
815
+ if (pollingStarted) {
816
+ throw this.createError(PhoneAuthErrorCode.INVALID_SESSION_STATE, 'Polling has already been started');
817
+ }
818
+ pollingStarted = true;
819
+ try {
820
+ const result = yield startPolling();
821
+ if (credentialResolve) {
822
+ credentialResolve(result);
823
+ }
824
+ return result;
825
+ }
826
+ catch (err) {
827
+ if (credentialReject) {
828
+ credentialReject(err);
829
+ }
830
+ throw err;
831
+ }
832
+ });
833
+ const wrappedStopPolling = () => {
834
+ handler.cleanup();
835
+ if (modal)
836
+ modal.close();
837
+ };
838
+ const wrappedCancel = () => {
839
+ handler.cancel();
840
+ handler.cleanup();
841
+ if (modal)
842
+ modal.close();
843
+ // Reject the credential promise if it's still pending
844
+ if (!pollingStarted && credentialReject) {
845
+ credentialReject(this.createError(PhoneAuthErrorCode.USER_DENIED, 'Desktop authentication cancelled by user'));
846
+ }
847
+ };
848
+ // Create the response object with a getter for is_polling
849
+ const response = {
793
850
  strategy: 'desktop',
794
851
  session: plainResponse.session,
795
852
  credential: credentialPromise,
796
853
  qr_code_data: qrCodeDataSnakeCase,
797
854
  modal_ref: modalRef,
798
- start_polling: startPolling,
799
- stop_polling: () => {
800
- handler.cleanup();
801
- if (modal)
802
- modal.close();
803
- },
804
- cancel: () => {
805
- handler.cancel();
806
- handler.cleanup();
807
- if (modal)
808
- modal.close();
809
- },
810
- is_polling: showModal // If modal shown, polling already started
855
+ start_polling: wrappedStartPolling,
856
+ stop_polling: wrappedStopPolling,
857
+ cancel: wrappedCancel,
858
+ // This will be replaced with a getter
859
+ is_polling: false // Initial value, will be overridden by getter
811
860
  };
861
+ // Define is_polling as a getter that returns current state
862
+ Object.defineProperty(response, 'is_polling', {
863
+ get() {
864
+ return handler.isPolling();
865
+ },
866
+ enumerable: true,
867
+ configurable: true
868
+ });
869
+ return response;
812
870
  }
813
871
  else {
814
872
  // Standard mode - return credential when complete
@@ -872,28 +930,62 @@ export class PhoneAuthClient {
872
930
  const pollingOptions = {
873
931
  pollingEndpoint: (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.pollingEndpoint) || ((_q = this.config.endpoints) === null || _q === void 0 ? void 0 : _q.polling),
874
932
  pollingInterval: (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.pollingInterval) || this.config.pollingInterval || 2000,
875
- maxPollingAttempts: (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.maxPollingAttempts) || this.config.maxPollingAttempts || 150,
933
+ maxPollingAttempts: (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.maxPollingAttempts) || this.config.maxPollingAttempts || 30,
876
934
  onLinkOpened: undefined,
877
935
  onStatusUpdate: undefined
878
936
  };
879
937
  console.log('[PhoneAuth Client] Final Link polling options:', pollingOptions);
880
- // Create credential promise
881
- const credentialPromise = handler.invoke(plainResponse, pollingOptions).then(result => {
882
- if (result.authenticated && result.credential) {
883
- return {
884
- credential: result.credential,
885
- session: plainResponse.session,
886
- authenticated: true
887
- };
888
- }
889
- else {
890
- throw this.createError(PhoneAuthErrorCode.USER_DENIED, result.error || 'Link authentication failed');
891
- }
892
- });
893
938
  // Handle based on execution mode
894
939
  if (executionMode === 'extended') {
895
- // Extended mode - return control methods
896
- return {
940
+ // Extended mode - return control methods without starting polling immediately
941
+ let credentialResolve = null;
942
+ let credentialReject = null;
943
+ let pollingStarted = false;
944
+ let pollingPromise = null;
945
+ // Create a pending promise that will be resolved when polling completes
946
+ const credentialPromise = new Promise((resolve, reject) => {
947
+ credentialResolve = resolve;
948
+ credentialReject = reject;
949
+ });
950
+ // Function to start polling (only once)
951
+ const startPolling = () => __awaiter(this, void 0, void 0, function* () {
952
+ if (pollingStarted) {
953
+ throw this.createError(PhoneAuthErrorCode.INVALID_SESSION_STATE, 'Polling has already been started');
954
+ }
955
+ pollingStarted = true;
956
+ try {
957
+ // Store the polling promise to ensure we use the same one
958
+ if (!pollingPromise) {
959
+ pollingPromise = handler.invoke(plainResponse, pollingOptions);
960
+ }
961
+ const result = yield pollingPromise;
962
+ if (result.authenticated && result.credential) {
963
+ const authCredential = {
964
+ credential: result.credential,
965
+ session: plainResponse.session,
966
+ authenticated: true
967
+ };
968
+ if (credentialResolve) {
969
+ credentialResolve(authCredential);
970
+ }
971
+ return authCredential;
972
+ }
973
+ else {
974
+ const error = this.createError(PhoneAuthErrorCode.USER_DENIED, result.error || 'Link authentication failed');
975
+ if (credentialReject) {
976
+ credentialReject(error);
977
+ }
978
+ throw error;
979
+ }
980
+ }
981
+ catch (err) {
982
+ if (credentialReject) {
983
+ credentialReject(err);
984
+ }
985
+ throw err;
986
+ }
987
+ });
988
+ const response = {
897
989
  strategy: 'link',
898
990
  session: plainResponse.session,
899
991
  credential: credentialPromise,
@@ -902,22 +994,44 @@ export class PhoneAuthClient {
902
994
  },
903
995
  // Re-open the app link
904
996
  trigger: triggerLink,
905
- // Polling control
906
- start_polling: () => handler.invoke(plainResponse, pollingOptions).then(result => ({
907
- credential: result.credential || '',
908
- session: plainResponse.session,
909
- authenticated: result.authenticated
910
- })),
997
+ // Polling control - now prevents double polling
998
+ start_polling: startPolling,
911
999
  stop_polling: () => handler.cleanup(),
912
1000
  cancel: () => {
913
1001
  handler.cancel();
914
1002
  handler.cleanup();
1003
+ // Reject the credential promise if polling hasn't started
1004
+ if (!pollingStarted && credentialReject) {
1005
+ credentialReject(this.createError(PhoneAuthErrorCode.USER_DENIED, 'Link authentication cancelled by user'));
1006
+ }
915
1007
  },
916
- is_polling: true
1008
+ // This will be replaced with a getter
1009
+ is_polling: false // Initial value, will be overridden by getter
917
1010
  };
1011
+ // Define is_polling as a getter that returns current state
1012
+ Object.defineProperty(response, 'is_polling', {
1013
+ get() {
1014
+ return handler.isPolling();
1015
+ },
1016
+ enumerable: true,
1017
+ configurable: true
1018
+ });
1019
+ return response;
918
1020
  }
919
1021
  else {
920
- // Standard mode - return credential when complete
1022
+ // Standard mode - start polling immediately and wait for completion
1023
+ const credentialPromise = handler.invoke(plainResponse, pollingOptions).then(result => {
1024
+ if (result.authenticated && result.credential) {
1025
+ return {
1026
+ credential: result.credential,
1027
+ session: plainResponse.session,
1028
+ authenticated: true
1029
+ };
1030
+ }
1031
+ else {
1032
+ throw this.createError(PhoneAuthErrorCode.USER_DENIED, result.error || 'Link authentication failed');
1033
+ }
1034
+ });
921
1035
  // Wait for credential and return in standard format
922
1036
  const credential = yield credentialPromise;
923
1037
  const aggregatorId = this.config.aggregatorId || 'default';