@glideidentity/web-client-sdk 5.0.1 → 5.1.1-beta.1

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.
Files changed (97) hide show
  1. package/README.md +8 -108
  2. package/dist/adapters/angular/index.js +1 -0
  3. package/dist/adapters/angular/phone-auth.service.d.ts +18 -0
  4. package/dist/adapters/angular/phone-auth.service.js +26 -0
  5. package/dist/adapters/react/index.js +3 -0
  6. package/dist/adapters/react/useClient.js +1 -0
  7. package/dist/adapters/react/usePhoneAuth.js +16 -1
  8. package/dist/adapters/vanilla/client.js +1 -0
  9. package/dist/adapters/vanilla/index.js +1 -0
  10. package/dist/adapters/vanilla/phone-auth.js +31 -0
  11. package/dist/adapters/vue/index.js +4 -0
  12. package/dist/adapters/vue/useClient.js +5 -0
  13. package/dist/adapters/vue/usePhoneAuth.js +20 -1
  14. package/dist/browser/web-client-sdk.min.js +1 -2
  15. package/dist/browser.js +6 -0
  16. package/dist/core/client.js +12 -0
  17. package/dist/core/logger.js +81 -1
  18. package/dist/core/phone-auth/api-types.d.ts +1 -4
  19. package/dist/core/phone-auth/api-types.js +83 -0
  20. package/dist/core/phone-auth/client.js +374 -38
  21. package/dist/core/phone-auth/error-utils.js +83 -1
  22. package/dist/core/phone-auth/index.d.ts +1 -1
  23. package/dist/core/phone-auth/index.js +2 -2
  24. package/dist/core/phone-auth/status-types.d.ts +78 -0
  25. package/dist/core/phone-auth/status-types.js +17 -0
  26. package/dist/core/phone-auth/strategies/desktop.d.ts +2 -0
  27. package/dist/core/phone-auth/strategies/desktop.js +136 -13
  28. package/dist/core/phone-auth/strategies/index.d.ts +4 -0
  29. package/dist/core/phone-auth/strategies/index.js +4 -0
  30. package/dist/core/phone-auth/strategies/link.d.ts +2 -0
  31. package/dist/core/phone-auth/strategies/link.js +97 -13
  32. package/dist/core/phone-auth/strategies/ts43.d.ts +19 -0
  33. package/dist/core/phone-auth/strategies/ts43.js +33 -2
  34. package/dist/core/phone-auth/strategies/types.js +4 -0
  35. package/dist/core/phone-auth/type-guards.js +131 -0
  36. package/dist/core/phone-auth/types.d.ts +5 -0
  37. package/dist/core/phone-auth/types.js +32 -0
  38. package/dist/core/phone-auth/ui/mobile-debug-console.js +28 -2
  39. package/dist/core/phone-auth/ui/modal.d.ts +55 -33
  40. package/dist/core/phone-auth/ui/modal.js +422 -889
  41. package/dist/core/phone-auth/validation-utils.d.ts +0 -9
  42. package/dist/core/phone-auth/validation-utils.js +34 -25
  43. package/dist/core/version.js +2 -1
  44. package/dist/esm/adapters/angular/index.js +1 -0
  45. package/dist/esm/adapters/angular/phone-auth.service.d.ts +18 -0
  46. package/dist/esm/adapters/angular/phone-auth.service.js +26 -0
  47. package/dist/esm/adapters/react/index.js +3 -0
  48. package/dist/esm/adapters/react/useClient.js +1 -0
  49. package/dist/esm/adapters/react/usePhoneAuth.js +16 -1
  50. package/dist/esm/adapters/vanilla/client.js +1 -0
  51. package/dist/esm/adapters/vanilla/index.js +1 -0
  52. package/dist/esm/adapters/vanilla/phone-auth.d.ts +24 -0
  53. package/dist/esm/adapters/vanilla/phone-auth.js +31 -0
  54. package/dist/esm/adapters/vue/index.js +4 -0
  55. package/dist/esm/adapters/vue/useClient.js +5 -0
  56. package/dist/esm/adapters/vue/usePhoneAuth.js +20 -1
  57. package/dist/esm/browser.js +6 -0
  58. package/dist/esm/core/client.d.ts +10 -0
  59. package/dist/esm/core/client.js +12 -0
  60. package/dist/esm/core/logger.d.ts +53 -0
  61. package/dist/esm/core/logger.js +81 -1
  62. package/dist/esm/core/phone-auth/api-types.d.ts +313 -1
  63. package/dist/esm/core/phone-auth/api-types.js +83 -0
  64. package/dist/esm/core/phone-auth/client.d.ts +144 -0
  65. package/dist/esm/core/phone-auth/client.js +375 -39
  66. package/dist/esm/core/phone-auth/error-utils.d.ts +29 -0
  67. package/dist/esm/core/phone-auth/error-utils.js +83 -1
  68. package/dist/esm/core/phone-auth/index.d.ts +1 -1
  69. package/dist/esm/core/phone-auth/index.js +4 -2
  70. package/dist/esm/core/phone-auth/status-types.d.ts +78 -0
  71. package/dist/esm/core/phone-auth/status-types.js +17 -0
  72. package/dist/esm/core/phone-auth/strategies/desktop.d.ts +65 -0
  73. package/dist/esm/core/phone-auth/strategies/desktop.js +136 -13
  74. package/dist/esm/core/phone-auth/strategies/index.d.ts +4 -0
  75. package/dist/esm/core/phone-auth/strategies/index.js +4 -0
  76. package/dist/esm/core/phone-auth/strategies/link.d.ts +50 -0
  77. package/dist/esm/core/phone-auth/strategies/link.js +97 -13
  78. package/dist/esm/core/phone-auth/strategies/ts43.d.ts +19 -0
  79. package/dist/esm/core/phone-auth/strategies/ts43.js +33 -2
  80. package/dist/esm/core/phone-auth/strategies/types.d.ts +13 -0
  81. package/dist/esm/core/phone-auth/strategies/types.js +4 -0
  82. package/dist/esm/core/phone-auth/type-guards.d.ts +128 -0
  83. package/dist/esm/core/phone-auth/type-guards.js +131 -0
  84. package/dist/esm/core/phone-auth/types.d.ts +113 -0
  85. package/dist/esm/core/phone-auth/types.js +32 -0
  86. package/dist/esm/core/phone-auth/ui/mobile-debug-console.d.ts +4 -0
  87. package/dist/esm/core/phone-auth/ui/mobile-debug-console.js +28 -2
  88. package/dist/esm/core/phone-auth/ui/modal.d.ts +68 -27
  89. package/dist/esm/core/phone-auth/ui/modal.js +422 -889
  90. package/dist/esm/core/phone-auth/validation-utils.d.ts +26 -4
  91. package/dist/esm/core/phone-auth/validation-utils.js +34 -24
  92. package/dist/esm/core/types.d.ts +35 -0
  93. package/dist/esm/core/version.js +2 -1
  94. package/dist/esm/index.js +9 -1
  95. package/dist/index.js +7 -0
  96. package/package.json +1 -1
  97. package/dist/browser/web-client-sdk.min.js.LICENSE.txt +0 -1
@@ -7,10 +7,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
+ // Import API types for API communication
10
11
  import * as API from './api-types';
11
12
  import { BrowserError, BrowserErrorCode, BrowserName } from './types';
12
13
  import { PhoneAuthErrorCode, isPhoneAuthError, parseBackendError, getUserMessage, isUserError, serializeError, createErrorBreadcrumb } from './error-utils';
13
- import { validatePhoneNumber, validatePlmn, validateNonce } from './validation-utils';
14
+ import { validatePhoneNumber, validatePlmn } from './validation-utils';
14
15
  import { LoggerFactory } from '../logger';
15
16
  import { DesktopHandler } from './strategies/desktop';
16
17
  import { LinkHandler } from './strategies/link';
@@ -21,31 +22,41 @@ export class PhoneAuthClient {
21
22
  this.crossDeviceActive = false;
22
23
  this.retryCount = 0;
23
24
  this.sessionCache = new Map();
25
+ // Store base timeout for normal operations
24
26
  this.baseTimeout = config.timeout || 30000;
27
+ // Default configuration with cross-device support
25
28
  this.config = {
26
29
  endpoints: {
27
30
  prepare: ((_a = config.endpoints) === null || _a === void 0 ? void 0 : _a.prepare) || '/api/magic-auth/prepare',
28
31
  process: ((_b = config.endpoints) === null || _b === void 0 ? void 0 : _b.process) || '/api/magic-auth/process',
29
- polling: (_c = config.endpoints) === null || _c === void 0 ? void 0 : _c.polling
32
+ polling: (_c = config.endpoints) === null || _c === void 0 ? void 0 : _c.polling // Pass through the polling endpoint
30
33
  },
31
34
  timeout: config.timeout || 30000,
32
- pollingInterval: config.pollingInterval || 2000,
33
- maxPollingAttempts: config.maxPollingAttempts || 30,
35
+ pollingInterval: config.pollingInterval || 2000, // Default 2 seconds
36
+ maxPollingAttempts: config.maxPollingAttempts || 30, // Changed from 150 to 30 (1 minute total)
34
37
  debug: config.debug || false,
35
38
  aggregatorId: config.aggregatorId || 'default',
39
+ devEnv: config.devEnv,
36
40
  devtools: config.devtools
37
41
  };
38
42
  this.debug = this.config.debug;
43
+ // Log initialization with devEnv if set
44
+ if (config.devEnv) {
45
+ console.log(`[PhoneAuth] Initialized with devEnv: ${config.devEnv}`);
46
+ }
47
+ // Store callbacks
39
48
  this.callbacks = {
40
49
  onCrossDeviceDetected: config.onCrossDeviceDetected,
41
50
  onRetryAttempt: config.onRetryAttempt
42
51
  };
52
+ // Initialize logger based on config
43
53
  this.logger = LoggerFactory.create({
44
54
  level: config.logLevel,
45
55
  prefix: '[PhoneAuth]',
46
56
  remote: config.remoteLogging,
47
57
  custom: config.logger
48
58
  });
59
+ // Initialize developer tools if configured
49
60
  if (((_d = config.devtools) === null || _d === void 0 ? void 0 : _d.showMobileConsole) && typeof window !== 'undefined') {
50
61
  import('./ui/mobile-debug-console').then(({ MobileDebugConsole }) => {
51
62
  MobileDebugConsole.init();
@@ -54,21 +65,32 @@ export class PhoneAuthClient {
54
65
  console.error('[PhoneAuth] Failed to load mobile debug console:', err);
55
66
  });
56
67
  }
68
+ // Set up session cache cleanup
57
69
  this.setupCacheCleanup();
58
70
  }
71
+ /**
72
+ * Get user-friendly error message using error utilities
73
+ */
59
74
  getUserFriendlyMessage(error) {
60
75
  if (typeof error === 'string') {
76
+ // For legacy string error codes
61
77
  return getUserMessage({ code: error });
62
78
  }
63
79
  return getUserMessage(error);
64
80
  }
81
+ /**
82
+ * Log error with proper context and sanitization
83
+ */
65
84
  logError(error, context) {
85
+ // Create breadcrumb for error tracking
66
86
  const breadcrumb = createErrorBreadcrumb(error);
87
+ // Serialize error for logging (sanitized)
67
88
  const serialized = serializeError(error);
68
89
  if (this.debug || !isUserError(error)) {
69
90
  console.error('[PhoneAuth] Error:', Object.assign(Object.assign({}, serialized), { breadcrumb,
70
91
  context }));
71
92
  }
93
+ // Log trace context for distributed tracing (if available)
72
94
  if (error.traceId) {
73
95
  console.debug('[PhoneAuth] Trace Context:', {
74
96
  traceId: error.traceId,
@@ -77,11 +99,20 @@ export class PhoneAuthClient {
77
99
  });
78
100
  }
79
101
  }
102
+ /**
103
+ * Check if the browser supports secure phone authentication
104
+ */
80
105
  isSupported() {
106
+ // Only check on client side
81
107
  if (typeof window === 'undefined')
82
108
  return false;
109
+ // Check for the DigitalCredential constructor specifically
110
+ // This is more accurate than checking credentials.get which exists for other credential types
83
111
  return 'DigitalCredential' in window;
84
112
  }
113
+ /**
114
+ * Get detailed browser support information
115
+ */
85
116
  getBrowserSupportInfo() {
86
117
  if (typeof window === 'undefined') {
87
118
  return {
@@ -100,6 +131,7 @@ export class PhoneAuthClient {
100
131
  browser: isChrome ? BrowserName.CHROME : isEdge ? BrowserName.EDGE : BrowserName.OTHER
101
132
  };
102
133
  }
134
+ // Provide specific guidance based on browser
103
135
  if (isChrome || isEdge) {
104
136
  return {
105
137
  supported: false,
@@ -116,11 +148,16 @@ export class PhoneAuthClient {
116
148
  message: 'Your browser doesn\'t support the Digital Credentials API. Please use Chrome or Edge with the #web-identity-digital-credentials flag enabled.'
117
149
  };
118
150
  }
151
+ /**
152
+ * Main verification method with silent retry support
153
+ */
119
154
  verify(options) {
120
155
  return __awaiter(this, void 0, void 0, function* () {
156
+ // Reset retry count for new verification
121
157
  this.retryCount = 0;
122
158
  this.lastRequest = options;
123
- const maxRetries = 2;
159
+ const maxRetries = 2; // Default max retries
160
+ // Try verification with silent retries
124
161
  return this.verifyWithRetry(options, maxRetries);
125
162
  });
126
163
  }
@@ -128,38 +165,55 @@ export class PhoneAuthClient {
128
165
  return __awaiter(this, void 0, void 0, function* () {
129
166
  var _a, _b;
130
167
  try {
168
+ // Step 1: Prepare the phone verification request
131
169
  const preparedRequest = yield this.preparePhoneRequest(options);
170
+ // Step 2: Invoke secure prompt for user consent (always in UI mode for high-level API)
132
171
  const credentialResponse = yield this.invokeSecurePrompt(preparedRequest);
172
+ // Check if headless result was returned (this shouldn't happen in high-level API)
133
173
  if (credentialResponse && typeof credentialResponse === 'object' && 'strategy' in credentialResponse) {
134
174
  throw this.createError(PhoneAuthErrorCode.INVALID_RESPONSE, 'Headless mode is not supported in authenticatePhoneNumber. Use preparePhoneRequest and invokeSecurePrompt directly for headless mode.');
135
175
  }
176
+ // Step 3: Process the response through appropriate endpoint
136
177
  const credential = credentialResponse;
178
+ // Validate use_case is provided for endpoint selection
137
179
  if (!options.use_case) {
138
180
  throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'use_case is required', { field: 'use_case' });
139
181
  }
140
182
  const result = options.use_case === API.USE_CASE.GET_PHONE_NUMBER
141
183
  ? yield this.getPhoneNumber(credential, preparedRequest.session)
142
184
  : yield this.verifyPhoneNumber(credential, preparedRequest.session);
185
+ // Return the result directly - it's already the correct type
186
+ // Cache successful result with session info for later use
143
187
  this.cacheSession(options, result);
144
188
  return result;
145
189
  }
146
190
  catch (error) {
147
191
  const authError = isPhoneAuthError(error) ? error : parseBackendError(error);
192
+ // Check if we should retry (silent retry - don't throw yet)
193
+ // Note: We cannot automatically retry USER_DENIED errors because the Digital Credentials API
194
+ // requires user interaction (transient activation). Automatic retries would fail with
195
+ // "The 'digital-credentials-get' feature requires transient activation" error.
148
196
  if (this.shouldRetry(authError) && this.retryCount < maxRetries) {
149
197
  this.retryCount++;
198
+ // Notify about retry attempt (but don't show error to user)
150
199
  (_b = (_a = this.callbacks).onRetryAttempt) === null || _b === void 0 ? void 0 : _b.call(_a, this.retryCount, maxRetries);
151
200
  if (this.debug) {
152
201
  console.log(`[PhoneAuth] Retrying verification (attempt ${this.retryCount + 1}/${maxRetries + 1})`);
153
202
  }
154
- yield this.delay(Math.min(1000 * Math.pow(2, this.retryCount - 1), 5000));
203
+ // Wait before retry
204
+ yield this.delay(Math.min(1000 * Math.pow(2, this.retryCount - 1), 5000)); // Exponential backoff
205
+ // Check cache for recent successful session
155
206
  const cachedResult = this.getCachedSession(options);
156
207
  if (cachedResult) {
157
208
  if (this.debug)
158
209
  console.log('[PhoneAuth] Using cached session result');
159
210
  return cachedResult;
160
211
  }
212
+ // Retry the verification
161
213
  return this.verifyWithRetry(options, maxRetries);
162
214
  }
215
+ // All retries exhausted or non-retryable error - now throw
216
+ // Add context
163
217
  authError.context = {
164
218
  step: 'complete',
165
219
  useCase: options.use_case,
@@ -168,11 +222,15 @@ export class PhoneAuthClient {
168
222
  attemptNumber: this.retryCount + 1,
169
223
  maxAttempts: maxRetries + 1
170
224
  };
225
+ // Log error with proper sanitization
171
226
  this.logError(authError, { options });
227
+ // Re-throw the structured error
172
228
  if (isPhoneAuthError(error)) {
229
+ // If it already has context, throw as-is
173
230
  if (error.context) {
174
231
  throw error;
175
232
  }
233
+ // Otherwise, create a new error with context
176
234
  const enhancedError = {
177
235
  code: authError.code,
178
236
  message: error.message,
@@ -197,35 +255,60 @@ export class PhoneAuthClient {
197
255
  }
198
256
  });
199
257
  }
258
+ /**
259
+ * High-level method to get phone number (complete flow)
260
+ * Handles prepare, credential prompt, and get phone number in one call
261
+ */
200
262
  getPhoneNumberComplete(options) {
201
263
  return __awaiter(this, void 0, void 0, function* () {
202
264
  return this.verify(Object.assign({ use_case: API.USE_CASE.GET_PHONE_NUMBER }, options));
203
265
  });
204
266
  }
267
+ /**
268
+ * High-level method to verify phone number (complete flow)
269
+ * Handles prepare, credential prompt, and verification in one call
270
+ */
205
271
  verifyPhoneNumberComplete(phoneNumber, options) {
206
272
  return __awaiter(this, void 0, void 0, function* () {
207
273
  return this.verify(Object.assign({ use_case: API.USE_CASE.VERIFY_PHONE_NUMBER, phone_number: phoneNumber }, options));
208
274
  });
209
275
  }
276
+ /**
277
+ * Step 1: Prepare phone verification request
278
+ *
279
+ * This method prepares a secure request for phone verification.
280
+ * You can use this with your own backend or the glide-sdk-node.
281
+ *
282
+ * @example
283
+ * ```typescript
284
+ * const request = await phoneAuthClient.preparePhoneRequest({ useCase: 'GetPhoneNumber' });
285
+ * // Handle the request with custom logic
286
+ * ```
287
+ */
210
288
  preparePhoneRequest(options) {
211
289
  return __awaiter(this, void 0, void 0, function* () {
212
290
  var _a, _b, _c;
291
+ // Validate phone number if provided
213
292
  if (options.phone_number) {
214
293
  const phoneValidation = validatePhoneNumber(options.phone_number);
215
294
  if (!phoneValidation.valid) {
216
295
  throw this.createError(PhoneAuthErrorCode.INVALID_PHONE_NUMBER, phoneValidation.error, { field: 'phone_number' });
217
296
  }
218
297
  }
298
+ // Validate PLMN if provided
219
299
  if (options.plmn) {
220
300
  const plmnValidation = validatePlmn(options.plmn);
221
301
  if (!plmnValidation.valid) {
222
302
  throw this.createError(PhoneAuthErrorCode.BAD_REQUEST, plmnValidation.error, { field: 'plmn' });
223
303
  }
224
304
  }
305
+ // Validate use_case is provided (unless only parent_session_id is given)
225
306
  if (!options.use_case && !(((_a = options.options) === null || _a === void 0 ? void 0 : _a.parent_session_id) && !options.phone_number && !options.plmn)) {
226
307
  throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'use_case is required', { field: 'use_case' });
227
308
  }
309
+ // Validate required parameters based on use case
228
310
  if (!options.phone_number && !options.plmn && !((_b = options.options) === null || _b === void 0 ? void 0 : _b.parent_session_id)) {
311
+ // Provide specific error message based on use case
229
312
  if (options.use_case === API.USE_CASE.GET_PHONE_NUMBER) {
230
313
  throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'PLMN (MCC/MNC) is required for GetPhoneNumber. Please provide carrier network information.', { field: 'plmn', useCase: 'GetPhoneNumber' });
231
314
  }
@@ -233,36 +316,39 @@ export class PhoneAuthClient {
233
316
  throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'Phone number is required for VerifyPhoneNumber', { field: 'phoneNumber', useCase: 'VerifyPhoneNumber' });
234
317
  }
235
318
  else {
319
+ // Fallback for other use cases
236
320
  throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'Either phone number or PLMN (MCC/MNC) must be provided', { field: 'phoneNumber,plmn' });
237
321
  }
238
322
  }
323
+ // Log parent session usage
239
324
  if (((_c = options.options) === null || _c === void 0 ? void 0 : _c.parent_session_id) && !options.phone_number && !options.plmn) {
240
325
  if (this.debug) {
241
326
  console.log('[PhoneAuth] Using parent_session_id: %s, use_case: %s', options.options.parent_session_id, options.use_case || 'not provided');
242
327
  }
243
328
  }
244
329
  const requestId = `web-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
245
- const nonce = btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32))))
246
- .replace(/\+/g, '-')
247
- .replace(/\//g, '_')
248
- .replace(/=/g, '');
249
- const nonceValidation = validateNonce(nonce);
250
- if (!nonceValidation.valid) {
251
- throw this.createError(PhoneAuthErrorCode.INTERNAL_SERVER_ERROR, 'Failed to generate valid nonce', { field: 'nonce' });
252
- }
330
+ // Build properly typed request body according to API specification
331
+ // Be permissive - backend will ignore extra fields if not needed
253
332
  const requestBody = {
333
+ // Include use_case if provided (optional when parent_session_id is given)
254
334
  use_case: options.use_case,
335
+ // Include phone_number if provided (backend ignores if not needed)
255
336
  phone_number: options.phone_number,
337
+ // Include PLMN if provided
256
338
  plmn: options.plmn ? {
257
339
  mcc: options.plmn.mcc,
258
340
  mnc: options.plmn.mnc
259
341
  } : undefined,
260
- nonce: nonce,
342
+ // Auto-generated request ID
261
343
  id: requestId,
344
+ // Optional fields
262
345
  client_info: {
263
346
  user_agent: navigator.userAgent,
264
- platform: navigator.platform
347
+ platform: navigator.platform,
348
+ // user_agent: "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
349
+ // platform: "Android"
265
350
  },
351
+ // Advanced options (for desktop-mobile binding and future features)
266
352
  options: options.options
267
353
  };
268
354
  this.log('Preparing phone verification request', requestBody);
@@ -273,15 +359,20 @@ export class PhoneAuthClient {
273
359
  body: JSON.stringify(requestBody)
274
360
  });
275
361
  if (!response.ok) {
362
+ // Try to get error details from response body
276
363
  let errorDetails = null;
277
364
  try {
278
365
  errorDetails = yield response.json();
366
+ // Always include the HTTP status from the response
279
367
  errorDetails = Object.assign(Object.assign({}, errorDetails), { status: response.status });
280
368
  }
281
369
  catch (_d) {
370
+ // If JSON parsing fails, use status text
282
371
  errorDetails = { status: response.status, statusText: response.statusText };
283
372
  }
373
+ // Parse the backend error response (handles both structured and unstructured errors)
284
374
  const parsedError = parseBackendError(errorDetails);
375
+ // Enhance with additional context
285
376
  parsedError.context = {
286
377
  step: 'prepare',
287
378
  useCase: options.use_case,
@@ -289,6 +380,7 @@ export class PhoneAuthClient {
289
380
  userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,
290
381
  url: typeof window !== 'undefined' ? window.location.href : undefined
291
382
  };
383
+ // Add endpoint info
292
384
  parsedError.details = Object.assign(Object.assign({}, parsedError.details), { endpoint: 'prepare', status: response.status });
293
385
  throw parsedError;
294
386
  }
@@ -297,12 +389,16 @@ export class PhoneAuthClient {
297
389
  if (!data.authentication_strategy || !data.data || !data.session) {
298
390
  throw this.createError(PhoneAuthErrorCode.INVALID_RESPONSE, 'Invalid response format from backend');
299
391
  }
392
+ // Return the full response as-is
393
+ // The invoke method will handle it based on authentication_strategy
300
394
  return data;
301
395
  }
302
396
  catch (error) {
397
+ // If it's already an AuthError, re-throw it
303
398
  if (this.isAuthError(error)) {
304
399
  throw error;
305
400
  }
401
+ // Otherwise, wrap it as a network error
306
402
  throw this.createError(PhoneAuthErrorCode.NETWORK_ERROR, 'Failed to prepare verification request', {
307
403
  originalError: error,
308
404
  context: {
@@ -316,16 +412,68 @@ export class PhoneAuthClient {
316
412
  }
317
413
  });
318
414
  }
415
+ /**
416
+ * Step 2: Invoke secure prompt for user consent
417
+ *
418
+ * This method can work in two modes:
419
+ * 1. **UI Mode (default)**: Shows built-in UI components (modals/buttons)
420
+ * 2. **Headless Mode**: Returns raw data for custom UI implementation
421
+ *
422
+ * **Important**: This method automatically handles reactive objects from frameworks
423
+ * like Vue.js and React by deep cloning the input. This ensures compatibility with
424
+ * browser APIs that expect plain objects.
425
+ *
426
+ * @example UI Mode (shows modal/button)
427
+ * ```typescript
428
+ * // Shows SDK's built-in UI
429
+ * const credential = await phoneAuth.invokeSecurePrompt(prepareResult);
430
+ *
431
+ * // Customize the UI
432
+ * const credential = await phoneAuth.invokeSecurePrompt(prepareResult, {
433
+ * modalOptions: {
434
+ * title: 'Verify Your Identity',
435
+ * buttonText: 'Continue with Verizon'
436
+ * }
437
+ * });
438
+ * ```
439
+ *
440
+ * @example Extended Mode (returns control methods)
441
+ * ```typescript
442
+ * // Get control methods for custom implementation
443
+ * const result = await phoneAuth.invokeSecurePrompt(prepareResult, {
444
+ * executionMode: 'extended',
445
+ * preventDefaultUI: true // Desktop: no modal
446
+ * });
447
+ *
448
+ * if (result.strategy === 'desktop') {
449
+ * // Show custom QR UI
450
+ * showCustomQR(result.qr_code_data);
451
+ * // Start polling
452
+ * await result.start_polling();
453
+ * }
454
+ * ```
455
+ *
456
+ * @param prepareResponse - Response from prepare() with strategy and data
457
+ * @param options - Control UI behavior and response type
458
+ * @returns Credential or ExtendedResponse based on executionMode
459
+ */
319
460
  invokeSecurePrompt(prepareResponse, options) {
320
461
  return __awaiter(this, void 0, void 0, function* () {
462
+ // Deep clone to avoid issues with reactive objects (Vue/React)
463
+ // This ensures we work with plain objects for browser APIs
464
+ // Vue's reactivity system wraps objects in Proxies which can interfere
465
+ // with browser APIs like Digital Credentials API that expect plain objects
321
466
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
467
+ // Try structuredClone first (modern browsers), but catch errors and fallback to JSON method
322
468
  let plainResponse;
323
469
  try {
470
+ // structuredClone might throw if object contains non-cloneable properties
324
471
  plainResponse = typeof structuredClone !== 'undefined'
325
472
  ? structuredClone(prepareResponse)
326
473
  : JSON.parse(JSON.stringify(prepareResponse));
327
474
  }
328
475
  catch (cloneError) {
476
+ // Fallback to JSON method if structuredClone fails
329
477
  if (this.debug) {
330
478
  console.log('[PhoneAuth] structuredClone failed, using JSON fallback:', cloneError);
331
479
  }
@@ -335,11 +483,16 @@ export class PhoneAuthClient {
335
483
  console.log('[PhoneAuth] Session cache size:', this.sessionCache.size);
336
484
  console.log('[PhoneAuth] Retry count:', this.retryCount);
337
485
  console.log('[PhoneAuth] PrepareResponse received:', JSON.stringify(plainResponse, null, 2));
486
+ // Treat options as InvokeOptions (the modern format)
487
+ // Legacy DesktopAuthOptions properties will still work through property access
338
488
  const opts = options;
489
+ // Get configuration from options - access properties directly
339
490
  const strategy = plainResponse.authentication_strategy;
340
491
  const preventDefaultUI = (_a = opts === null || opts === void 0 ? void 0 : opts.preventDefaultUI) !== null && _a !== void 0 ? _a : false;
341
492
  const executionMode = (_b = opts === null || opts === void 0 ? void 0 : opts.executionMode) !== null && _b !== void 0 ? _b : 'standard';
493
+ // Handle based on authentication strategy
342
494
  if (plainResponse.authentication_strategy === API.AUTHENTICATION_STRATEGY.TS43) {
495
+ // Check browser support for TS43 strategy which requires Digital Credentials API
343
496
  if (!this.isSupported()) {
344
497
  throw this.createError(PhoneAuthErrorCode.BROWSER_NOT_SUPPORTED, 'Your browser does not support the Digital Credentials API required for TS43 authentication');
345
498
  }
@@ -353,13 +506,18 @@ export class PhoneAuthClient {
353
506
  }
354
507
  };
355
508
  this.log('Invoking TS43 secure authentication prompt', secureCredentialRequest);
509
+ // Function to trigger TS43 authentication
356
510
  const triggerTS43 = () => __awaiter(this, void 0, void 0, function* () {
357
511
  var _a, _b;
358
512
  try {
513
+ // This is the browser API call for TS43
514
+ // Cast to CredentialRequestOptions with digital field (TS43 specific)
359
515
  const credentialOptions = secureCredentialRequest;
360
516
  const credentialResponse = yield navigator.credentials.get(credentialOptions);
517
+ // Type guard for Digital Credential response
361
518
  const digitalResponse = credentialResponse;
362
519
  if (!digitalResponse || !('data' in digitalResponse) || !digitalResponse.data) {
520
+ // Check if this is likely due to the browser flag being disabled
363
521
  const supportInfo = this.getBrowserSupportInfo();
364
522
  if (supportInfo.browser === BrowserName.CHROME || supportInfo.browser === BrowserName.EDGE) {
365
523
  throw new Error(`Digital Credentials API returned no response. This usually means the browser feature flag is not enabled. Please ensure the ${supportInfo.helpUrl || '#web-identity-digital-credentials flag'} is set to "Enabled" (not "Default" or "Disabled") and restart your browser.`);
@@ -371,6 +529,7 @@ export class PhoneAuthClient {
371
529
  return credentialData.vp_token;
372
530
  }
373
531
  catch (error) {
532
+ // Capture detailed browser error information
374
533
  const errorObj = error;
375
534
  const browserErrorDetails = {
376
535
  name: errorObj.name || 'UnknownError',
@@ -383,9 +542,11 @@ export class PhoneAuthClient {
383
542
  timestamp: new Date().toISOString(),
384
543
  userAgent: navigator.userAgent,
385
544
  url: window.location.href,
545
+ // Include request details for debugging
386
546
  authentication_strategy: plainResponse.authentication_strategy,
387
547
  hasSession: !!plainResponse.session
388
548
  };
549
+ // Handle specific browser errors
389
550
  if (errorObj.name === BrowserError.NOT_ALLOWED) {
390
551
  throw this.createError(PhoneAuthErrorCode.USER_DENIED, 'User denied the credential request or the request timed out', {
391
552
  originalError: error,
@@ -393,6 +554,7 @@ export class PhoneAuthClient {
393
554
  context: errorContext
394
555
  });
395
556
  }
557
+ // NetworkError with code 19 specifically indicates user cancellation in Digital Credentials API
396
558
  if (errorObj.name === BrowserError.NETWORK && errorObj.code === BrowserErrorCode.USER_CANCELLED_DC_API) {
397
559
  throw this.createError(PhoneAuthErrorCode.USER_DENIED, 'Authentication cancelled by user', {
398
560
  originalError: error,
@@ -400,6 +562,7 @@ export class PhoneAuthClient {
400
562
  context: errorContext
401
563
  });
402
564
  }
565
+ // NetworkError without code 19 is a real network error
403
566
  if (errorObj.name === BrowserError.NETWORK) {
404
567
  throw this.createError(PhoneAuthErrorCode.NETWORK_ERROR, 'Network error occurred while retrieving credentials', {
405
568
  originalError: error,
@@ -421,6 +584,7 @@ export class PhoneAuthClient {
421
584
  context: errorContext
422
585
  });
423
586
  }
587
+ // Check for other cancellation patterns
424
588
  if (errorObj.name === BrowserError.ABORT ||
425
589
  ((_a = browserErrorDetails.message) === null || _a === void 0 ? void 0 : _a.includes('The operation was aborted')) ||
426
590
  ((_b = browserErrorDetails.message) === null || _b === void 0 ? void 0 : _b.includes('User cancelled'))) {
@@ -430,6 +594,7 @@ export class PhoneAuthClient {
430
594
  context: errorContext
431
595
  });
432
596
  }
597
+ // For any other errors, capture all details
433
598
  throw this.createError(PhoneAuthErrorCode.INTERNAL_SERVER_ERROR, `Digital Credentials API error: ${errorObj.message || 'Unknown error'}`, {
434
599
  originalError: error,
435
600
  browserError: browserErrorDetails,
@@ -437,6 +602,11 @@ export class PhoneAuthClient {
437
602
  });
438
603
  }
439
604
  });
605
+ // IMPORTANT: For TS43, we ALWAYS call the API directly without any modal
606
+ // The Digital Credentials API provides its own OS-level UI (drawer/bottom sheet)
607
+ // Adding our own modal would be redundant and confusing for users
608
+ // Unlike Link (which needs a button for iOS App Clips), TS43 just needs direct invocation
609
+ // Enhanced trigger function with callback support
440
610
  const enhancedTriggerTS43 = () => __awaiter(this, void 0, void 0, function* () {
441
611
  var _a, _b;
442
612
  try {
@@ -456,11 +626,16 @@ export class PhoneAuthClient {
456
626
  throw error;
457
627
  }
458
628
  });
629
+ // TS43 always auto-triggers (no SDK UI ever)
630
+ // The Digital Credentials API provides its own OS-level UI
631
+ // Use a wrapper object to allow updating the promise reference
459
632
  const credentialWrapper = {
460
633
  promise: null
461
634
  };
462
635
  try {
636
+ // Always try to trigger immediately
463
637
  const vpToken = yield enhancedTriggerTS43();
638
+ // Convert to AuthCredential format
464
639
  credentialWrapper.promise = Promise.resolve({
465
640
  credential: typeof vpToken === 'string' ? vpToken : Object.values(vpToken)[0],
466
641
  session: plainResponse.session,
@@ -468,25 +643,34 @@ export class PhoneAuthClient {
468
643
  });
469
644
  }
470
645
  catch (error) {
646
+ // If auto-trigger fails, create a rejected promise
471
647
  credentialWrapper.promise = Promise.reject(error);
472
648
  }
649
+ // Handle based on execution mode
473
650
  if (executionMode === 'extended') {
651
+ // Extended mode - return control methods
474
652
  const response = {
475
653
  strategy: 'ts43',
476
654
  session: plainResponse.session,
477
- credential: credentialWrapper.promise,
655
+ credential: credentialWrapper.promise, // Initial value
656
+ // Re-trigger credential request
478
657
  trigger: () => __awaiter(this, void 0, void 0, function* () {
479
658
  const vpToken = yield enhancedTriggerTS43();
659
+ // Update the credential promise in the wrapper
480
660
  credentialWrapper.promise = Promise.resolve({
481
661
  credential: typeof vpToken === 'string' ? vpToken : Object.values(vpToken)[0],
482
662
  session: plainResponse.session,
483
663
  authenticated: true
484
664
  });
665
+ // Return void as per interface
485
666
  }),
486
667
  cancel: () => {
668
+ // TS43 doesn't have a way to cancel once triggered
669
+ // but we can reject the promise
487
670
  credentialWrapper.promise = Promise.reject(this.createError(PhoneAuthErrorCode.USER_DENIED, 'Authentication cancelled'));
488
671
  }
489
672
  };
673
+ // Define credential as a getter that always returns the current promise
490
674
  Object.defineProperty(response, 'credential', {
491
675
  get() {
492
676
  return credentialWrapper.promise;
@@ -497,15 +681,20 @@ export class PhoneAuthClient {
497
681
  return response;
498
682
  }
499
683
  else {
684
+ // Standard mode - just return credential
685
+ // Wait for and return the credential
500
686
  const credential = yield credentialWrapper.promise;
687
+ // Return in standard format
501
688
  return {
502
689
  [plainResponse.session.session_key]: credential.credential
503
690
  };
504
691
  }
505
692
  }
506
693
  else if (plainResponse.authentication_strategy === API.AUTHENTICATION_STRATEGY.DESKTOP) {
694
+ // Desktop strategy - QR code based authentication
507
695
  const desktopData = plainResponse.data;
508
696
  const handler = new DesktopHandler();
697
+ // Extract QR code data - convert to QRCodeData format for modal
509
698
  const qrCodeData = {
510
699
  iosQRCode: ((_c = desktopData.data) === null || _c === void 0 ? void 0 : _c.ios_qr_image) || desktopData.ios_qr_image ||
511
700
  ((_d = desktopData.data) === null || _d === void 0 ? void 0 : _d.qr_code_image) || desktopData.qr_code_image || desktopData.qr_code || '',
@@ -513,15 +702,18 @@ export class PhoneAuthClient {
513
702
  iosUrl: ((_f = desktopData.data) === null || _f === void 0 ? void 0 : _f.ios_url) || desktopData.ios_url,
514
703
  androidUrl: ((_g = desktopData.data) === null || _g === void 0 ? void 0 : _g.android_url) || desktopData.android_url
515
704
  };
705
+ // Also keep snake_case format for extended response
516
706
  const qrCodeDataSnakeCase = {
517
707
  ios_qr_image: ((_h = desktopData.data) === null || _h === void 0 ? void 0 : _h.ios_qr_image) || desktopData.ios_qr_image,
518
708
  android_qr_image: ((_j = desktopData.data) === null || _j === void 0 ? void 0 : _j.android_qr_image) || desktopData.android_qr_image,
519
709
  qr_code: ((_k = desktopData.data) === null || _k === void 0 ? void 0 : _k.qr_code_image) || desktopData.qr_code_image || desktopData.qr_code,
520
710
  challenge: (_l = desktopData.data) === null || _l === void 0 ? void 0 : _l.challenge
521
711
  };
712
+ // Polling options - gather from any options format
522
713
  const pollingEndpointToUse = (opts === null || opts === void 0 ? void 0 : opts.pollingEndpoint) ||
523
714
  ((_m = this.config.endpoints) === null || _m === void 0 ? void 0 : _m.polling);
524
- const pollingOptions = Object.assign(Object.assign({}, opts), { pollingEndpoint: pollingEndpointToUse, pollingInterval: (opts === null || opts === void 0 ? void 0 : opts.pollingInterval) || this.config.pollingInterval || 2000, maxPollingAttempts: (opts === null || opts === void 0 ? void 0 : opts.maxPollingAttempts) || this.config.maxPollingAttempts || 30, onQRCodeReady: undefined, onStatusUpdate: undefined });
715
+ const pollingOptions = Object.assign(Object.assign({}, opts), { pollingEndpoint: pollingEndpointToUse, pollingInterval: (opts === null || opts === void 0 ? void 0 : opts.pollingInterval) || this.config.pollingInterval || 2000, maxPollingAttempts: (opts === null || opts === void 0 ? void 0 : opts.maxPollingAttempts) || this.config.maxPollingAttempts || 30, devEnv: (opts === null || opts === void 0 ? void 0 : opts.devEnv) || this.config.devEnv, onQRCodeReady: undefined, onStatusUpdate: undefined });
716
+ // Decide whether to show modal based on preventDefaultUI
525
717
  const showModal = !preventDefaultUI;
526
718
  let modal;
527
719
  let modalRef = undefined;
@@ -533,27 +725,40 @@ export class PhoneAuthClient {
533
725
  });
534
726
  if (showModal) {
535
727
  console.log('[Desktop] Creating modal with QR data:', qrCodeData);
728
+ // Create and setup modal
536
729
  modal = new AuthModal(opts === null || opts === void 0 ? void 0 : opts.modalOptions, opts === null || opts === void 0 ? void 0 : opts.callbacks);
537
730
  modal.setCloseCallback(() => {
538
731
  this.log('Desktop QR modal closed by user, cancelling polling');
539
732
  handler.cancel();
540
733
  });
734
+ // Add UI callbacks to polling options
541
735
  pollingOptions.onQRCodeReady = (qrData) => {
542
736
  console.log('[Desktop] onQRCodeReady callback triggered:', qrData);
543
737
  modal.showQRCode(qrData, 'Scan with your mobile device');
544
738
  };
545
739
  pollingOptions.onStatusUpdate = (status) => {
546
- if (status.status === 'authenticated' ||
547
- status.status === 'error' ||
548
- status.status === 'expired') {
549
- modal.close();
740
+ if (status.status === 'pending') {
741
+ modal.updateStatus('Waiting for authentication...');
742
+ }
743
+ else if (status.status === 'authenticated') {
744
+ modal.updateStatus('Authentication successful!');
745
+ setTimeout(() => modal.close(), 1500);
746
+ }
747
+ else if (status.status === 'expired') {
748
+ modal.updateStatus('QR code expired', true);
749
+ }
750
+ else if (status.status === 'error') {
751
+ modal.updateStatus('Authentication failed', true);
550
752
  }
551
753
  };
754
+ // Note: We don't show the QR code here. It will be shown by the onQRCodeReady callback
755
+ // that gets triggered immediately when handler.invoke() is called
552
756
  modalRef = modal;
553
757
  }
554
758
  else {
555
759
  console.log('[Desktop] Modal not shown - preventDefaultUI is true');
556
760
  }
761
+ // Create credential promise
557
762
  const startPolling = () => handler.invoke(plainResponse, pollingOptions).then(result => {
558
763
  if (result.authenticated && result.credential) {
559
764
  return {
@@ -566,13 +771,22 @@ export class PhoneAuthClient {
566
771
  throw this.createError(PhoneAuthErrorCode.USER_DENIED, result.error || 'Desktop authentication failed');
567
772
  }
568
773
  });
774
+ // Handle based on execution mode
569
775
  if (executionMode === 'extended') {
776
+ // Extended mode - return control methods
777
+ // Create a promise and always start polling immediately
570
778
  const credentialPromise = new Promise((resolve, reject) => {
779
+ // Always start polling immediately in extended mode (consistent with Link)
780
+ // This prevents the "unresolved promise trap" where developers might await
781
+ // the credential before calling start_polling()
571
782
  startPolling()
572
783
  .then(resolve)
573
784
  .catch(reject);
574
785
  });
786
+ // Create wrapped functions
575
787
  const wrappedStartPolling = () => __awaiter(this, void 0, void 0, function* () {
788
+ // Polling has already started automatically in extended mode
789
+ // This method just returns the existing credential promise for consistency
576
790
  return credentialPromise;
577
791
  });
578
792
  const wrappedStopPolling = () => {
@@ -585,7 +799,9 @@ export class PhoneAuthClient {
585
799
  handler.cleanup();
586
800
  if (modal)
587
801
  modal.close();
802
+ // The credential promise will be rejected by the handler.cancel() call
588
803
  };
804
+ // Create the response object with a getter for is_polling
589
805
  const response = {
590
806
  strategy: 'desktop',
591
807
  session: plainResponse.session,
@@ -595,8 +811,10 @@ export class PhoneAuthClient {
595
811
  start_polling: wrappedStartPolling,
596
812
  stop_polling: wrappedStopPolling,
597
813
  cancel: wrappedCancel,
598
- is_polling: false
814
+ // This will be replaced with a getter
815
+ is_polling: false // Initial value, will be overridden by getter
599
816
  };
817
+ // Define is_polling as a getter that returns current state
600
818
  Object.defineProperty(response, 'is_polling', {
601
819
  get() {
602
820
  return handler.isPolling();
@@ -607,8 +825,11 @@ export class PhoneAuthClient {
607
825
  return response;
608
826
  }
609
827
  else {
828
+ // Standard mode - return credential when complete
829
+ // Start polling and wait for result
610
830
  try {
611
831
  const credential = yield startPolling();
832
+ // Extract session ID for compatibility
612
833
  let sessionId = 'default';
613
834
  if (desktopData && typeof desktopData === 'object') {
614
835
  if (desktopData.data && typeof desktopData.data === 'object') {
@@ -628,8 +849,10 @@ export class PhoneAuthClient {
628
849
  }
629
850
  }
630
851
  else if (plainResponse.authentication_strategy === API.AUTHENTICATION_STRATEGY.LINK) {
852
+ // Link strategy - app-based authentication (iOS/Android)
631
853
  const linkData = plainResponse.data;
632
854
  const handler = new LinkHandler();
855
+ // Create reusable trigger function that ONLY opens the App Clip
633
856
  const triggerLink = () => {
634
857
  var _a, _b;
635
858
  try {
@@ -649,9 +872,12 @@ export class PhoneAuthClient {
649
872
  });
650
873
  }
651
874
  };
875
+ // Link always auto-opens the app (no SDK UI by default)
876
+ // Open immediately to preserve user gesture context
652
877
  if ((opts === null || opts === void 0 ? void 0 : opts.autoTrigger) !== false) {
653
878
  triggerLink();
654
879
  }
880
+ // Start polling in the background
655
881
  console.log('[PhoneAuth Client] Link polling config:', {
656
882
  fromOptions: opts === null || opts === void 0 ? void 0 : opts.pollingEndpoint,
657
883
  fromClientConfig: (_o = this.config.endpoints) === null || _o === void 0 ? void 0 : _o.polling,
@@ -661,15 +887,20 @@ export class PhoneAuthClient {
661
887
  pollingEndpoint: (opts === null || opts === void 0 ? void 0 : opts.pollingEndpoint) || ((_q = this.config.endpoints) === null || _q === void 0 ? void 0 : _q.polling),
662
888
  pollingInterval: (opts === null || opts === void 0 ? void 0 : opts.pollingInterval) || this.config.pollingInterval || 2000,
663
889
  maxPollingAttempts: (opts === null || opts === void 0 ? void 0 : opts.maxPollingAttempts) || this.config.maxPollingAttempts || 30,
890
+ devEnv: (opts === null || opts === void 0 ? void 0 : opts.devEnv) || this.config.devEnv,
664
891
  onLinkOpened: undefined,
665
892
  onStatusUpdate: undefined
666
893
  };
667
894
  console.log('[PhoneAuth Client] Final Link polling options:', pollingOptions);
895
+ // Handle based on execution mode
668
896
  if (executionMode === 'extended') {
897
+ // Extended mode - return control methods and start polling immediately
669
898
  let pollingStarted = false;
670
899
  let pollingPromise = null;
900
+ // Start polling immediately (Link always polls automatically unlike Desktop)
671
901
  pollingPromise = handler.invoke(plainResponse, pollingOptions);
672
902
  pollingStarted = true;
903
+ // Create credential promise from the polling result
673
904
  const credentialPromise = pollingPromise.then(result => {
674
905
  if (result.authenticated && result.credential) {
675
906
  return {
@@ -682,8 +913,10 @@ export class PhoneAuthClient {
682
913
  throw this.createError(PhoneAuthErrorCode.USER_DENIED, result.error || 'Link authentication failed');
683
914
  }
684
915
  });
916
+ // Function to restart polling (for retry scenarios)
685
917
  const startPolling = () => __awaiter(this, void 0, void 0, function* () {
686
918
  if (!pollingStarted) {
919
+ // This is here for API consistency, but for Link it's already polling
687
920
  pollingStarted = true;
688
921
  if (!pollingPromise) {
689
922
  pollingPromise = handler.invoke(plainResponse, pollingOptions);
@@ -700,6 +933,7 @@ export class PhoneAuthClient {
700
933
  throw this.createError(PhoneAuthErrorCode.USER_DENIED, result.error || 'Link authentication failed');
701
934
  }
702
935
  }
936
+ // If already polling, just return the existing promise result
703
937
  const result = yield pollingPromise;
704
938
  if (result.authenticated && result.credential) {
705
939
  return {
@@ -719,15 +953,21 @@ export class PhoneAuthClient {
719
953
  data: {
720
954
  app_url: linkData.url
721
955
  },
956
+ // Re-open the app link
722
957
  trigger: triggerLink,
958
+ // Polling control - now prevents double polling
723
959
  start_polling: startPolling,
724
960
  stop_polling: () => handler.cleanup(),
725
961
  cancel: () => {
726
962
  handler.cancel();
727
963
  handler.cleanup();
964
+ // Note: For Link, polling is already started, so the promise
965
+ // will be rejected by the handler.cancel() call
728
966
  },
729
- is_polling: false
967
+ // This will be replaced with a getter
968
+ is_polling: false // Initial value, will be overridden by getter
730
969
  };
970
+ // Define is_polling as a getter that returns current state
731
971
  Object.defineProperty(response, 'is_polling', {
732
972
  get() {
733
973
  return handler.isPolling();
@@ -738,6 +978,7 @@ export class PhoneAuthClient {
738
978
  return response;
739
979
  }
740
980
  else {
981
+ // Standard mode - start polling immediately and wait for completion
741
982
  const credentialPromise = handler.invoke(plainResponse, pollingOptions).then(result => {
742
983
  if (result.authenticated && result.credential) {
743
984
  return {
@@ -750,28 +991,44 @@ export class PhoneAuthClient {
750
991
  throw this.createError(PhoneAuthErrorCode.USER_DENIED, result.error || 'Link authentication failed');
751
992
  }
752
993
  });
994
+ // Wait for credential and return in standard format
753
995
  const credential = yield credentialPromise;
754
996
  const aggregatorId = this.config.aggregatorId || 'default';
755
997
  return { [aggregatorId]: credential.credential };
756
998
  }
757
999
  }
758
1000
  else {
1001
+ // Unknown strategy
759
1002
  throw this.createError(PhoneAuthErrorCode.UNSUPPORTED_STRATEGY, `Unknown authentication strategy: ${plainResponse.authentication_strategy}`);
760
1003
  }
761
1004
  });
762
1005
  }
1006
+ /**
1007
+ * Step 3A: Get phone number from credential
1008
+ *
1009
+ * @example
1010
+ * ```typescript
1011
+ * const prepareResp = await phoneAuthClient.preparePhoneRequest({ useCase: 'GetPhoneNumber', plmn: {...} });
1012
+ * const credential = await phoneAuthClient.invokeSecurePrompt(prepareResp);
1013
+ * const result = await phoneAuthClient.getPhoneNumber(credential, prepareResp.session);
1014
+ * console.log(result.phone_number); // +1234567890
1015
+ * ```
1016
+ */
763
1017
  getPhoneNumber(credentialResponse, session) {
764
1018
  return __awaiter(this, void 0, void 0, function* () {
1019
+ // Extract credential string
765
1020
  const credentialString = this.extractCredentialString(credentialResponse);
1021
+ // Build request body for GetPhoneNumber
766
1022
  const requestBody = {
767
1023
  session: session,
768
1024
  credential: credentialString,
769
- use_case: API.USE_CASE.GET_PHONE_NUMBER
1025
+ use_case: API.USE_CASE.GET_PHONE_NUMBER // Required for server routing
770
1026
  };
1027
+ // Only show full details in debug mode, mask sensitive data otherwise
771
1028
  if (this.config.debug) {
772
1029
  this.log('Getting phone number from credential', {
773
1030
  session: session,
774
- credential: credentialString ? credentialString.substring(0, 50) + '...' : 'undefined',
1031
+ credential: credentialString ? credentialString.substring(0, 50) + '...' : 'undefined', // Show partial for debugging
775
1032
  endpoint: this.config.endpoints.process || '/api/phone-auth/process'
776
1033
  });
777
1034
  }
@@ -815,18 +1072,35 @@ export class PhoneAuthClient {
815
1072
  }
816
1073
  });
817
1074
  }
1075
+ /**
1076
+ * Step 3B: Verify phone number with credential
1077
+ *
1078
+ * @example
1079
+ * ```typescript
1080
+ * const prepareResp = await phoneAuthClient.preparePhoneRequest({
1081
+ * useCase: 'VerifyPhoneNumber',
1082
+ * phoneNumber: '+1234567890'
1083
+ * });
1084
+ * const credential = await phoneAuthClient.invokeSecurePrompt(prepareResp);
1085
+ * const result = await phoneAuthClient.verifyPhoneNumber(credential, prepareResp.session);
1086
+ * console.log(result.verified); // true
1087
+ * ```
1088
+ */
818
1089
  verifyPhoneNumber(credentialResponse, session) {
819
1090
  return __awaiter(this, void 0, void 0, function* () {
1091
+ // Extract credential string
820
1092
  const credentialString = this.extractCredentialString(credentialResponse);
1093
+ // Build request body for VerifyPhoneNumber
821
1094
  const requestBody = {
822
1095
  session: session,
823
1096
  credential: credentialString,
824
- use_case: API.USE_CASE.VERIFY_PHONE_NUMBER
1097
+ use_case: API.USE_CASE.VERIFY_PHONE_NUMBER // Required for server routing
825
1098
  };
1099
+ // Only show full details in debug mode, mask sensitive data otherwise
826
1100
  if (this.config.debug) {
827
1101
  this.log('Verifying phone number with credential', {
828
1102
  session: session,
829
- credential: credentialString ? credentialString.substring(0, 50) + '...' : 'undefined',
1103
+ credential: credentialString ? credentialString.substring(0, 50) + '...' : 'undefined', // Show partial for debugging
830
1104
  endpoint: this.config.endpoints.process || '/api/phone-auth/process'
831
1105
  });
832
1106
  }
@@ -873,36 +1147,53 @@ export class PhoneAuthClient {
873
1147
  }
874
1148
  });
875
1149
  }
1150
+ /**
1151
+ * Helper to extract credential string from various formats
1152
+ */
876
1153
  extractCredentialString(credentialResponse) {
1154
+ // If already a string, return it
877
1155
  if (typeof credentialResponse === 'string') {
878
1156
  return credentialResponse;
879
1157
  }
1158
+ // Extract from vp_token object - try configured aggregatorId first, then 'glide', then 'default'
880
1159
  let credential = credentialResponse[this.config.aggregatorId || 'glide'];
1160
+ // Fallback to 'glide' if not found
881
1161
  if (!credential && credentialResponse['glide']) {
882
1162
  credential = credentialResponse['glide'];
883
1163
  }
1164
+ // Fallback to 'default' if still not found
884
1165
  if (!credential && credentialResponse['default']) {
885
1166
  credential = credentialResponse['default'];
886
1167
  }
1168
+ // If still not found, try to get the first available key
887
1169
  if (!credential) {
888
1170
  const keys = Object.keys(credentialResponse);
889
1171
  if (keys.length > 0) {
890
1172
  credential = credentialResponse[keys[0]];
891
1173
  }
892
1174
  }
1175
+ // Convert array to string if needed
893
1176
  return Array.isArray(credential) ? credential[0] : credential;
894
1177
  }
1178
+ /**
1179
+ * Helper to extract error details from response
1180
+ */
895
1181
  extractErrorDetails(response) {
896
1182
  return __awaiter(this, void 0, void 0, function* () {
897
1183
  try {
898
1184
  const errorData = yield response.json();
899
- return Object.assign(Object.assign({}, errorData), { status: response.status });
1185
+ // Always include the HTTP status from the response
1186
+ return Object.assign(Object.assign({}, errorData), { status: response.status // Ensure HTTP status is included
1187
+ });
900
1188
  }
901
1189
  catch (_a) {
902
1190
  return { status: response.status, statusText: response.statusText };
903
1191
  }
904
1192
  });
905
1193
  }
1194
+ /**
1195
+ * Fetch with timeout
1196
+ */
906
1197
  fetchWithTimeout(url, options) {
907
1198
  return __awaiter(this, void 0, void 0, function* () {
908
1199
  const controller = new AbortController();
@@ -915,18 +1206,25 @@ export class PhoneAuthClient {
915
1206
  }
916
1207
  });
917
1208
  }
1209
+ /**
1210
+ * Create an AuthError
1211
+ */
918
1212
  createError(code, message, details) {
919
1213
  const error = {
920
1214
  code,
921
1215
  message,
922
- details: (details === null || details === void 0 ? void 0 : details.originalError) ? Object.assign(Object.assign({}, details), { originalError: undefined }) : details
1216
+ details: (details === null || details === void 0 ? void 0 : details.originalError) ? Object.assign(Object.assign({}, details), { originalError: undefined // Remove the original error object
1217
+ }) : details
923
1218
  };
1219
+ // Add browser error details if present
924
1220
  if (details === null || details === void 0 ? void 0 : details.browserError) {
925
1221
  error.browserError = details.browserError;
926
1222
  }
1223
+ // Add context if present
927
1224
  if (details === null || details === void 0 ? void 0 : details.context) {
928
1225
  error.context = details.context;
929
1226
  }
1227
+ // Add other specific fields
930
1228
  if (details === null || details === void 0 ? void 0 : details.status) {
931
1229
  error.status = details.status;
932
1230
  }
@@ -941,37 +1239,51 @@ export class PhoneAuthClient {
941
1239
  }
942
1240
  return error;
943
1241
  }
1242
+ /**
1243
+ * Type guard for AuthError
1244
+ */
944
1245
  isAuthError(error) {
945
1246
  return error && typeof error.code === 'string' && typeof error.message === 'string';
946
1247
  }
1248
+ /**
1249
+ * Debug logging
1250
+ */
947
1251
  log(...args) {
948
1252
  if (this.debug) {
949
1253
  console.log('[PhoneAuth]', ...args);
950
1254
  }
951
1255
  }
1256
+ /**
1257
+ * Determine if an error should trigger a retry
1258
+ */
952
1259
  shouldRetry(error) {
953
1260
  var _a, _b, _c;
1261
+ // Don't retry on 4xx client errors (these are not transient)
954
1262
  if (error.status && error.status >= 400 && error.status < 500) {
955
1263
  return false;
956
1264
  }
1265
+ // Don't retry on explicit user denial or unsupported browser
1266
+ // USER_DENIED cannot be retried automatically because the Digital Credentials API
1267
+ // requires user interaction (transient activation)
957
1268
  const nonRetryableCodes = [
958
1269
  'USER_DENIED',
959
1270
  'BROWSER_NOT_SUPPORTED',
960
1271
  'INVALID_PHONE_NUMBER',
961
1272
  'INVALID_PARAMETERS',
962
1273
  'MISSING_PARAMETERS',
963
- 'UNPROCESSABLE_ENTITY',
964
- 'USE_CASE_MISMATCH',
965
- 'PHONE_NUMBER_MISMATCH',
966
- 'VERIFICATION_FAILED',
967
- 'INVALID_CREDENTIAL',
968
- 'CARRIER_NOT_ELIGIBLE',
969
- 'SESSION_EXPIRED',
970
- 'INVALID_SESSION'
1274
+ 'UNPROCESSABLE_ENTITY', // 422 errors
1275
+ 'USE_CASE_MISMATCH', // Use case validation errors
1276
+ 'PHONE_NUMBER_MISMATCH', // Phone verification failures
1277
+ 'VERIFICATION_FAILED', // Verification failures
1278
+ 'INVALID_CREDENTIAL', // Bad credentials
1279
+ 'CARRIER_NOT_ELIGIBLE', // Carrier not supported
1280
+ 'SESSION_EXPIRED', // Session expired
1281
+ 'INVALID_SESSION' // Invalid session
971
1282
  ];
972
1283
  if (nonRetryableCodes.includes(error.code)) {
973
1284
  return false;
974
1285
  }
1286
+ // Only retry on network and server errors (5xx)
975
1287
  const retryableCodes = [
976
1288
  'NETWORK_ERROR',
977
1289
  'REQUEST_TIMEOUT',
@@ -980,6 +1292,7 @@ export class PhoneAuthClient {
980
1292
  'BAD_GATEWAY',
981
1293
  'INTERNAL_SERVER_ERROR'
982
1294
  ];
1295
+ // Also check for cross-device error types in details
983
1296
  const isCrossDeviceError = ((_a = error.details) === null || _a === void 0 ? void 0 : _a.errorType) && [
984
1297
  'CROSS_DEVICE_TIMEOUT',
985
1298
  'CROSS_DEVICE_CONNECTION_LOST',
@@ -989,13 +1302,17 @@ export class PhoneAuthClient {
989
1302
  isCrossDeviceError ||
990
1303
  (((_b = error.browserError) === null || _b === void 0 ? void 0 : _b.name) === 'NetworkError' && ((_c = error.browserError) === null || _c === void 0 ? void 0 : _c.code) !== 19);
991
1304
  }
1305
+ /**
1306
+ * Analyze and enhance errors specific to cross-device flows
1307
+ */
992
1308
  analyzeCrossDeviceError(error, prepareResponse) {
993
1309
  var _a;
994
1310
  const errorObj = error;
1311
+ // Check for specific cross-device error patterns
995
1312
  const isCrossDeviceTimeout = (errorObj.name === 'AbortError' && this.crossDeviceActive) ||
996
1313
  (((_a = errorObj.message) === null || _a === void 0 ? void 0 : _a.includes('timeout')) && this.crossDeviceActive);
997
1314
  const isCrossDeviceNetworkIssue = errorObj.name === 'NetworkError' &&
998
- errorObj.code !== 19 &&
1315
+ errorObj.code !== 19 && // Not user cancellation
999
1316
  this.crossDeviceActive;
1000
1317
  if (isCrossDeviceTimeout) {
1001
1318
  return {
@@ -1023,8 +1340,12 @@ export class PhoneAuthClient {
1023
1340
  browserError: error.browserError
1024
1341
  };
1025
1342
  }
1343
+ // Return the original error if not cross-device specific
1026
1344
  return error;
1027
1345
  }
1346
+ /**
1347
+ * Cache successful session for retry scenarios
1348
+ */
1028
1349
  cacheSession(options, result) {
1029
1350
  const cacheKey = this.getCacheKey(options);
1030
1351
  this.sessionCache.set(cacheKey, {
@@ -1032,11 +1353,15 @@ export class PhoneAuthClient {
1032
1353
  data: result
1033
1354
  });
1034
1355
  }
1356
+ /**
1357
+ * Retrieve cached session if available and recent
1358
+ */
1035
1359
  getCachedSession(options) {
1036
1360
  const cacheKey = this.getCacheKey(options);
1037
1361
  const cached = this.sessionCache.get(cacheKey);
1038
1362
  if (!cached)
1039
1363
  return null;
1364
+ // Cache valid for 5 minutes
1040
1365
  const cacheValidMs = 5 * 60 * 1000;
1041
1366
  if (Date.now() - cached.timestamp > cacheValidMs) {
1042
1367
  this.sessionCache.delete(cacheKey);
@@ -1044,14 +1369,22 @@ export class PhoneAuthClient {
1044
1369
  }
1045
1370
  return cached.data;
1046
1371
  }
1372
+ /**
1373
+ * Generate cache key for session storage
1374
+ */
1047
1375
  getCacheKey(options) {
1048
1376
  var _a, _b;
1049
1377
  return `${options.use_case}-${options.phone_number || 'no-phone'}-${((_a = options.plmn) === null || _a === void 0 ? void 0 : _a.mcc) || ''}-${((_b = options.plmn) === null || _b === void 0 ? void 0 : _b.mnc) || ''}`;
1050
1378
  }
1379
+ /**
1380
+ * Set up periodic cache cleanup
1381
+ */
1051
1382
  setupCacheCleanup() {
1383
+ // Only run cleanup in browser environment (not during SSR)
1052
1384
  if (typeof window === 'undefined') {
1053
1385
  return;
1054
1386
  }
1387
+ // Clean up expired cache entries every minute
1055
1388
  setInterval(() => {
1056
1389
  const now = Date.now();
1057
1390
  const cacheValidMs = 5 * 60 * 1000;
@@ -1062,6 +1395,9 @@ export class PhoneAuthClient {
1062
1395
  }
1063
1396
  }, 60000);
1064
1397
  }
1398
+ /**
1399
+ * Utility delay function
1400
+ */
1065
1401
  delay(ms) {
1066
1402
  return new Promise(resolve => setTimeout(resolve, ms));
1067
1403
  }