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

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