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