@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.
- package/README.md +938 -0
- package/dist/adapters/angular/client.service.d.ts +7 -0
- package/dist/adapters/angular/client.service.js +30 -0
- package/dist/adapters/angular/index.d.ts +3 -0
- package/dist/adapters/angular/index.js +18 -0
- package/dist/adapters/angular/phone-auth.service.d.ts +38 -0
- package/dist/adapters/angular/phone-auth.service.js +130 -0
- package/dist/adapters/react/index.d.ts +9 -0
- package/dist/adapters/react/index.js +28 -0
- package/dist/adapters/react/useClient.d.ts +26 -0
- package/dist/adapters/react/useClient.js +121 -0
- package/dist/adapters/react/usePhoneAuth.d.ts +23 -0
- package/dist/adapters/react/usePhoneAuth.js +95 -0
- package/dist/adapters/vanilla/client.d.ts +8 -0
- package/dist/adapters/vanilla/client.js +33 -0
- package/dist/adapters/vanilla/index.d.ts +3 -0
- package/dist/adapters/vanilla/index.js +18 -0
- package/dist/adapters/vanilla/phone-auth.d.ts +46 -0
- package/dist/adapters/vanilla/phone-auth.js +138 -0
- package/dist/adapters/vue/index.d.ts +10 -0
- package/dist/adapters/vue/index.js +36 -0
- package/dist/adapters/vue/useClient.d.ts +115 -0
- package/dist/adapters/vue/useClient.js +131 -0
- package/dist/adapters/vue/usePhoneAuth.d.ts +94 -0
- package/dist/adapters/vue/usePhoneAuth.js +103 -0
- package/dist/browser/web-client-sdk.min.js +2 -0
- package/dist/browser/web-client-sdk.min.js.LICENSE.txt +1 -0
- package/dist/browser.d.ts +7 -0
- package/dist/browser.js +31 -0
- package/dist/core/client.d.ts +22 -0
- package/dist/core/client.js +77 -0
- package/dist/core/logger.d.ts +130 -0
- package/dist/core/logger.js +370 -0
- package/dist/core/phone-auth/api-types.d.ts +525 -0
- package/dist/core/phone-auth/api-types.js +215 -0
- package/dist/core/phone-auth/client.d.ts +187 -0
- package/dist/core/phone-auth/client.js +1353 -0
- package/dist/core/phone-auth/error-utils.d.ts +110 -0
- package/dist/core/phone-auth/error-utils.js +350 -0
- package/dist/core/phone-auth/index.d.ts +7 -0
- package/dist/core/phone-auth/index.js +47 -0
- package/dist/core/phone-auth/status-types.d.ts +107 -0
- package/dist/core/phone-auth/status-types.js +31 -0
- package/dist/core/phone-auth/strategies/desktop.d.ts +113 -0
- package/dist/core/phone-auth/strategies/desktop.js +502 -0
- package/dist/core/phone-auth/strategies/index.d.ts +11 -0
- package/dist/core/phone-auth/strategies/index.js +15 -0
- package/dist/core/phone-auth/strategies/link.d.ts +81 -0
- package/dist/core/phone-auth/strategies/link.js +265 -0
- package/dist/core/phone-auth/strategies/ts43.d.ts +32 -0
- package/dist/core/phone-auth/strategies/ts43.js +146 -0
- package/dist/core/phone-auth/strategies/types.d.ts +18 -0
- package/dist/core/phone-auth/strategies/types.js +6 -0
- package/dist/core/phone-auth/type-guards.d.ts +125 -0
- package/dist/core/phone-auth/type-guards.js +160 -0
- package/dist/core/phone-auth/types.d.ts +232 -0
- package/dist/core/phone-auth/types.js +93 -0
- package/dist/core/phone-auth/ui/mobile-debug-console.d.ts +25 -0
- package/dist/core/phone-auth/ui/mobile-debug-console.js +288 -0
- package/dist/core/phone-auth/ui/modal.d.ts +84 -0
- package/dist/core/phone-auth/ui/modal.js +574 -0
- package/dist/core/phone-auth/validation-utils.d.ts +66 -0
- package/dist/core/phone-auth/validation-utils.js +182 -0
- package/dist/core/types.d.ts +62 -0
- package/dist/core/types.js +2 -0
- package/dist/core/version.d.ts +1 -0
- package/dist/core/version.js +5 -0
- package/dist/esm/adapters/angular/client.service.d.ts +7 -0
- package/dist/esm/adapters/angular/client.service.js +27 -0
- package/dist/esm/adapters/angular/index.d.ts +3 -0
- package/dist/esm/adapters/angular/index.js +4 -0
- package/dist/esm/adapters/angular/phone-auth.service.d.ts +38 -0
- package/dist/esm/adapters/angular/phone-auth.service.js +127 -0
- package/dist/esm/adapters/react/index.d.ts +9 -0
- package/dist/esm/adapters/react/index.js +8 -0
- package/dist/esm/adapters/react/useClient.d.ts +26 -0
- package/dist/esm/adapters/react/useClient.js +116 -0
- package/dist/esm/adapters/react/usePhoneAuth.d.ts +23 -0
- package/dist/esm/adapters/react/usePhoneAuth.js +92 -0
- package/dist/esm/adapters/vanilla/client.d.ts +8 -0
- package/dist/esm/adapters/vanilla/client.js +29 -0
- package/dist/esm/adapters/vanilla/index.d.ts +3 -0
- package/dist/esm/adapters/vanilla/index.js +4 -0
- package/dist/esm/adapters/vanilla/phone-auth.d.ts +46 -0
- package/dist/esm/adapters/vanilla/phone-auth.js +134 -0
- package/dist/esm/adapters/vue/index.d.ts +10 -0
- package/dist/esm/adapters/vue/index.js +11 -0
- package/dist/esm/adapters/vue/useClient.d.ts +115 -0
- package/dist/esm/adapters/vue/useClient.js +127 -0
- package/dist/esm/adapters/vue/usePhoneAuth.d.ts +94 -0
- package/dist/esm/adapters/vue/usePhoneAuth.js +100 -0
- package/dist/esm/browser.d.ts +7 -0
- package/dist/esm/browser.js +11 -0
- package/dist/esm/core/client.d.ts +22 -0
- package/dist/esm/core/client.js +70 -0
- package/dist/esm/core/logger.d.ts +130 -0
- package/dist/esm/core/logger.js +359 -0
- package/dist/esm/core/phone-auth/api-types.d.ts +525 -0
- package/dist/esm/core/phone-auth/api-types.js +203 -0
- package/dist/esm/core/phone-auth/client.d.ts +187 -0
- package/dist/esm/core/phone-auth/client.js +1316 -0
- package/dist/esm/core/phone-auth/error-utils.d.ts +110 -0
- package/dist/esm/core/phone-auth/error-utils.js +338 -0
- package/dist/esm/core/phone-auth/index.d.ts +7 -0
- package/dist/esm/core/phone-auth/index.js +6 -0
- package/dist/esm/core/phone-auth/status-types.d.ts +107 -0
- package/dist/esm/core/phone-auth/status-types.js +26 -0
- package/dist/esm/core/phone-auth/strategies/desktop.d.ts +113 -0
- package/dist/esm/core/phone-auth/strategies/desktop.js +496 -0
- package/dist/esm/core/phone-auth/strategies/index.d.ts +11 -0
- package/dist/esm/core/phone-auth/strategies/index.js +7 -0
- package/dist/esm/core/phone-auth/strategies/link.d.ts +81 -0
- package/dist/esm/core/phone-auth/strategies/link.js +261 -0
- package/dist/esm/core/phone-auth/strategies/ts43.d.ts +32 -0
- package/dist/esm/core/phone-auth/strategies/ts43.js +142 -0
- package/dist/esm/core/phone-auth/strategies/types.d.ts +18 -0
- package/dist/esm/core/phone-auth/strategies/types.js +5 -0
- package/dist/esm/core/phone-auth/type-guards.d.ts +125 -0
- package/dist/esm/core/phone-auth/type-guards.js +150 -0
- package/dist/esm/core/phone-auth/types.d.ts +232 -0
- package/dist/esm/core/phone-auth/types.js +76 -0
- package/dist/esm/core/phone-auth/ui/mobile-debug-console.d.ts +25 -0
- package/dist/esm/core/phone-auth/ui/mobile-debug-console.js +284 -0
- package/dist/esm/core/phone-auth/ui/modal.d.ts +84 -0
- package/dist/esm/core/phone-auth/ui/modal.js +570 -0
- package/dist/esm/core/phone-auth/validation-utils.d.ts +66 -0
- package/dist/esm/core/phone-auth/validation-utils.js +174 -0
- package/dist/esm/core/types.d.ts +62 -0
- package/dist/esm/core/types.js +1 -0
- package/dist/esm/core/version.d.ts +1 -0
- package/dist/esm/core/version.js +2 -0
- package/dist/esm/index.d.ts +12 -0
- package/dist/esm/index.js +15 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +52 -0
- 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
|
+
}
|