@glideidentity/web-client-sdk 5.1.3 → 6.0.0-beta.2

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 (203) hide show
  1. package/README.md +337 -526
  2. package/dist/browser/web-client-sdk.min.js +1 -1
  3. package/dist/cjs/adapters/index.js +15 -0
  4. package/dist/cjs/adapters/react.js +192 -0
  5. package/dist/cjs/adapters/vanilla.js +38 -0
  6. package/dist/cjs/adapters/vue.js +187 -0
  7. package/dist/cjs/browser.js +58 -0
  8. package/dist/cjs/client/http.js +159 -0
  9. package/dist/cjs/client/index.js +19 -0
  10. package/dist/cjs/client/logger.js +135 -0
  11. package/dist/cjs/client/phone-auth-client.js +428 -0
  12. package/dist/cjs/client/strategies/polling.js +177 -0
  13. package/dist/cjs/core/errors.js +204 -0
  14. package/dist/cjs/core/index.js +83 -0
  15. package/dist/cjs/core/type-guards.js +196 -0
  16. package/dist/cjs/core/types.js +25 -0
  17. package/dist/{core/phone-auth/validation-utils.js → cjs/core/validators.js} +70 -23
  18. package/dist/cjs/index.js +81 -0
  19. package/dist/cjs/ui/index.js +11 -0
  20. package/dist/{core/phone-auth → cjs}/ui/mobile-debug-console.js +149 -78
  21. package/dist/cjs/ui/modal.js +1122 -0
  22. package/dist/esm/adapters/index.js +11 -0
  23. package/dist/esm/adapters/react.js +182 -0
  24. package/dist/esm/adapters/vanilla.js +29 -0
  25. package/dist/esm/adapters/vue.js +177 -0
  26. package/dist/esm/browser.js +30 -11
  27. package/dist/esm/client/http.js +156 -0
  28. package/dist/esm/client/index.js +11 -0
  29. package/dist/esm/client/logger.js +131 -0
  30. package/dist/esm/client/phone-auth-client.js +424 -0
  31. package/dist/esm/client/strategies/polling.js +174 -0
  32. package/dist/esm/core/errors.js +193 -0
  33. package/dist/esm/core/index.js +60 -0
  34. package/dist/esm/core/type-guards.js +181 -0
  35. package/dist/esm/core/types.js +22 -1
  36. package/dist/esm/core/{phone-auth/validation-utils.js → validators.js} +66 -21
  37. package/dist/esm/index.js +45 -17
  38. package/dist/esm/ui/index.js +5 -0
  39. package/dist/esm/{core/phone-auth/ui → ui}/mobile-debug-console.js +149 -78
  40. package/dist/esm/ui/modal.js +1117 -0
  41. package/dist/types/adapters/index.d.ts +10 -0
  42. package/dist/types/adapters/index.d.ts.map +1 -0
  43. package/dist/types/adapters/react.d.ts +70 -0
  44. package/dist/types/adapters/react.d.ts.map +1 -0
  45. package/dist/types/adapters/vanilla.d.ts +29 -0
  46. package/dist/types/adapters/vanilla.d.ts.map +1 -0
  47. package/dist/types/adapters/vue.d.ts +71 -0
  48. package/dist/types/adapters/vue.d.ts.map +1 -0
  49. package/dist/types/browser.d.ts +27 -0
  50. package/dist/types/browser.d.ts.map +1 -0
  51. package/dist/types/client/http.d.ts +41 -0
  52. package/dist/types/client/http.d.ts.map +1 -0
  53. package/dist/types/client/index.d.ts +10 -0
  54. package/dist/types/client/index.d.ts.map +1 -0
  55. package/dist/types/client/logger.d.ts +36 -0
  56. package/dist/types/client/logger.d.ts.map +1 -0
  57. package/dist/types/client/phone-auth-client.d.ts +91 -0
  58. package/dist/types/client/phone-auth-client.d.ts.map +1 -0
  59. package/dist/types/client/strategies/polling.d.ts +36 -0
  60. package/dist/types/client/strategies/polling.d.ts.map +1 -0
  61. package/dist/types/core/errors.d.ts +71 -0
  62. package/dist/types/core/errors.d.ts.map +1 -0
  63. package/dist/types/core/index.d.ts +38 -0
  64. package/dist/types/core/index.d.ts.map +1 -0
  65. package/dist/types/core/type-guards.d.ts +118 -0
  66. package/dist/types/core/type-guards.d.ts.map +1 -0
  67. package/dist/types/core/types.d.ts +535 -0
  68. package/dist/types/core/types.d.ts.map +1 -0
  69. package/dist/types/core/validators.d.ts +63 -0
  70. package/dist/types/core/validators.d.ts.map +1 -0
  71. package/dist/types/index.d.ts +40 -0
  72. package/dist/types/index.d.ts.map +1 -0
  73. package/dist/types/ui/index.d.ts +6 -0
  74. package/dist/types/ui/index.d.ts.map +1 -0
  75. package/dist/{esm/core/phone-auth → types}/ui/mobile-debug-console.d.ts +1 -0
  76. package/dist/types/ui/mobile-debug-console.d.ts.map +1 -0
  77. package/dist/types/ui/modal.d.ts +87 -0
  78. package/dist/types/ui/modal.d.ts.map +1 -0
  79. package/package.json +48 -34
  80. package/dist/adapters/angular/client.service.d.ts +0 -7
  81. package/dist/adapters/angular/client.service.js +0 -30
  82. package/dist/adapters/angular/index.d.ts +0 -3
  83. package/dist/adapters/angular/index.js +0 -18
  84. package/dist/adapters/angular/phone-auth.service.d.ts +0 -38
  85. package/dist/adapters/angular/phone-auth.service.js +0 -130
  86. package/dist/adapters/react/index.d.ts +0 -9
  87. package/dist/adapters/react/index.js +0 -28
  88. package/dist/adapters/react/useClient.d.ts +0 -26
  89. package/dist/adapters/react/useClient.js +0 -121
  90. package/dist/adapters/react/usePhoneAuth.d.ts +0 -23
  91. package/dist/adapters/react/usePhoneAuth.js +0 -95
  92. package/dist/adapters/vanilla/client.d.ts +0 -8
  93. package/dist/adapters/vanilla/client.js +0 -33
  94. package/dist/adapters/vanilla/index.d.ts +0 -3
  95. package/dist/adapters/vanilla/index.js +0 -18
  96. package/dist/adapters/vanilla/phone-auth.d.ts +0 -46
  97. package/dist/adapters/vanilla/phone-auth.js +0 -138
  98. package/dist/adapters/vue/index.d.ts +0 -10
  99. package/dist/adapters/vue/index.js +0 -36
  100. package/dist/adapters/vue/useClient.d.ts +0 -115
  101. package/dist/adapters/vue/useClient.js +0 -131
  102. package/dist/adapters/vue/usePhoneAuth.d.ts +0 -94
  103. package/dist/adapters/vue/usePhoneAuth.js +0 -103
  104. package/dist/browser.d.ts +0 -7
  105. package/dist/browser.js +0 -31
  106. package/dist/core/client.d.ts +0 -22
  107. package/dist/core/client.js +0 -77
  108. package/dist/core/logger.d.ts +0 -130
  109. package/dist/core/logger.js +0 -370
  110. package/dist/core/phone-auth/api-types.d.ts +0 -593
  111. package/dist/core/phone-auth/api-types.js +0 -215
  112. package/dist/core/phone-auth/client.d.ts +0 -189
  113. package/dist/core/phone-auth/client.js +0 -1441
  114. package/dist/core/phone-auth/error-utils.d.ts +0 -110
  115. package/dist/core/phone-auth/error-utils.js +0 -350
  116. package/dist/core/phone-auth/index.d.ts +0 -7
  117. package/dist/core/phone-auth/index.js +0 -50
  118. package/dist/core/phone-auth/status-types.d.ts +0 -107
  119. package/dist/core/phone-auth/status-types.js +0 -31
  120. package/dist/core/phone-auth/strategies/desktop.d.ts +0 -122
  121. package/dist/core/phone-auth/strategies/desktop.js +0 -596
  122. package/dist/core/phone-auth/strategies/index.d.ts +0 -11
  123. package/dist/core/phone-auth/strategies/index.js +0 -15
  124. package/dist/core/phone-auth/strategies/link.d.ts +0 -89
  125. package/dist/core/phone-auth/strategies/link.js +0 -384
  126. package/dist/core/phone-auth/strategies/ts43.d.ts +0 -32
  127. package/dist/core/phone-auth/strategies/ts43.js +0 -161
  128. package/dist/core/phone-auth/strategies/types.d.ts +0 -18
  129. package/dist/core/phone-auth/strategies/types.js +0 -6
  130. package/dist/core/phone-auth/type-guards.d.ts +0 -143
  131. package/dist/core/phone-auth/type-guards.js +0 -198
  132. package/dist/core/phone-auth/types.d.ts +0 -237
  133. package/dist/core/phone-auth/types.js +0 -93
  134. package/dist/core/phone-auth/ui/mobile-debug-console.d.ts +0 -25
  135. package/dist/core/phone-auth/ui/modal.d.ts +0 -88
  136. package/dist/core/phone-auth/ui/modal.js +0 -598
  137. package/dist/core/phone-auth/validation-utils.d.ts +0 -44
  138. package/dist/core/types.d.ts +0 -62
  139. package/dist/core/types.js +0 -2
  140. package/dist/core/version.d.ts +0 -1
  141. package/dist/core/version.js +0 -5
  142. package/dist/esm/adapters/angular/client.service.d.ts +0 -7
  143. package/dist/esm/adapters/angular/client.service.js +0 -27
  144. package/dist/esm/adapters/angular/index.d.ts +0 -3
  145. package/dist/esm/adapters/angular/index.js +0 -4
  146. package/dist/esm/adapters/angular/phone-auth.service.d.ts +0 -38
  147. package/dist/esm/adapters/angular/phone-auth.service.js +0 -127
  148. package/dist/esm/adapters/react/index.d.ts +0 -9
  149. package/dist/esm/adapters/react/index.js +0 -8
  150. package/dist/esm/adapters/react/useClient.d.ts +0 -26
  151. package/dist/esm/adapters/react/useClient.js +0 -116
  152. package/dist/esm/adapters/react/usePhoneAuth.d.ts +0 -23
  153. package/dist/esm/adapters/react/usePhoneAuth.js +0 -92
  154. package/dist/esm/adapters/vanilla/client.d.ts +0 -8
  155. package/dist/esm/adapters/vanilla/client.js +0 -29
  156. package/dist/esm/adapters/vanilla/index.d.ts +0 -3
  157. package/dist/esm/adapters/vanilla/index.js +0 -4
  158. package/dist/esm/adapters/vanilla/phone-auth.d.ts +0 -46
  159. package/dist/esm/adapters/vanilla/phone-auth.js +0 -134
  160. package/dist/esm/adapters/vue/index.d.ts +0 -10
  161. package/dist/esm/adapters/vue/index.js +0 -11
  162. package/dist/esm/adapters/vue/useClient.d.ts +0 -115
  163. package/dist/esm/adapters/vue/useClient.js +0 -127
  164. package/dist/esm/adapters/vue/usePhoneAuth.d.ts +0 -94
  165. package/dist/esm/adapters/vue/usePhoneAuth.js +0 -100
  166. package/dist/esm/browser.d.ts +0 -7
  167. package/dist/esm/core/client.d.ts +0 -22
  168. package/dist/esm/core/client.js +0 -70
  169. package/dist/esm/core/logger.d.ts +0 -130
  170. package/dist/esm/core/logger.js +0 -359
  171. package/dist/esm/core/phone-auth/api-types.d.ts +0 -593
  172. package/dist/esm/core/phone-auth/api-types.js +0 -203
  173. package/dist/esm/core/phone-auth/client.d.ts +0 -189
  174. package/dist/esm/core/phone-auth/client.js +0 -1404
  175. package/dist/esm/core/phone-auth/error-utils.d.ts +0 -110
  176. package/dist/esm/core/phone-auth/error-utils.js +0 -338
  177. package/dist/esm/core/phone-auth/index.d.ts +0 -7
  178. package/dist/esm/core/phone-auth/index.js +0 -8
  179. package/dist/esm/core/phone-auth/status-types.d.ts +0 -107
  180. package/dist/esm/core/phone-auth/status-types.js +0 -26
  181. package/dist/esm/core/phone-auth/strategies/desktop.d.ts +0 -122
  182. package/dist/esm/core/phone-auth/strategies/desktop.js +0 -590
  183. package/dist/esm/core/phone-auth/strategies/index.d.ts +0 -11
  184. package/dist/esm/core/phone-auth/strategies/index.js +0 -7
  185. package/dist/esm/core/phone-auth/strategies/link.d.ts +0 -89
  186. package/dist/esm/core/phone-auth/strategies/link.js +0 -380
  187. package/dist/esm/core/phone-auth/strategies/ts43.d.ts +0 -32
  188. package/dist/esm/core/phone-auth/strategies/ts43.js +0 -157
  189. package/dist/esm/core/phone-auth/strategies/types.d.ts +0 -18
  190. package/dist/esm/core/phone-auth/strategies/types.js +0 -5
  191. package/dist/esm/core/phone-auth/type-guards.d.ts +0 -143
  192. package/dist/esm/core/phone-auth/type-guards.js +0 -185
  193. package/dist/esm/core/phone-auth/types.d.ts +0 -237
  194. package/dist/esm/core/phone-auth/types.js +0 -76
  195. package/dist/esm/core/phone-auth/ui/modal.d.ts +0 -88
  196. package/dist/esm/core/phone-auth/ui/modal.js +0 -594
  197. package/dist/esm/core/phone-auth/validation-utils.d.ts +0 -44
  198. package/dist/esm/core/types.d.ts +0 -62
  199. package/dist/esm/core/version.d.ts +0 -1
  200. package/dist/esm/core/version.js +0 -2
  201. package/dist/esm/index.d.ts +0 -12
  202. package/dist/index.d.ts +0 -12
  203. package/dist/index.js +0 -55
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Logger for Phone Authentication SDK.
3
+ *
4
+ * Provides a simple console-based logger with:
5
+ * - Debug mode toggle
6
+ * - PII sanitization
7
+ * - Configurable prefix
8
+ */
9
+ // ============================================================================
10
+ // PII SANITIZATION
11
+ // ============================================================================
12
+ /**
13
+ * Patterns to detect and sanitize PII.
14
+ */
15
+ const PII_PATTERNS = {
16
+ // Phone numbers: +1234567890 or variations
17
+ phone: /(\+?[1-9]\d{6,14})/g,
18
+ // Email addresses
19
+ email: /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g,
20
+ // JWT tokens (three base64 segments separated by dots)
21
+ jwt: /(eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*)/g,
22
+ // Session keys (long alphanumeric strings)
23
+ sessionKey: /session[_-]?key["']?\s*[:=]\s*["']?([a-zA-Z0-9_-]{20,})["']?/gi,
24
+ };
25
+ /**
26
+ * Sanitize data to remove PII before logging.
27
+ */
28
+ function sanitize(data) {
29
+ if (data === null || data === undefined) {
30
+ return data;
31
+ }
32
+ if (typeof data === 'string') {
33
+ let sanitized = data;
34
+ // Mask phone numbers: +1234567890 -> +1***7890
35
+ sanitized = sanitized.replace(PII_PATTERNS.phone, (match) => {
36
+ if (match.length < 8)
37
+ return match; // Too short to be a phone
38
+ const visible = 4;
39
+ return match.slice(0, 2) + '***' + match.slice(-visible);
40
+ });
41
+ // Mask emails: user@example.com -> u***@example.com
42
+ sanitized = sanitized.replace(PII_PATTERNS.email, (match) => {
43
+ const [local, domain] = match.split('@');
44
+ return local.slice(0, 1) + '***@' + domain;
45
+ });
46
+ // Mask JWTs: eyJ... -> [JWT]
47
+ sanitized = sanitized.replace(PII_PATTERNS.jwt, '[JWT]');
48
+ // Mask session keys
49
+ sanitized = sanitized.replace(PII_PATTERNS.sessionKey, (match, key) => {
50
+ return match.replace(key, key.slice(0, 4) + '***' + key.slice(-4));
51
+ });
52
+ return sanitized;
53
+ }
54
+ if (Array.isArray(data)) {
55
+ return data.map(sanitize);
56
+ }
57
+ if (typeof data === 'object') {
58
+ const sanitized = {};
59
+ for (const [key, value] of Object.entries(data)) {
60
+ // Completely mask sensitive field values
61
+ const sensitiveFields = ['phone_number', 'phoneNumber', 'credential', 'token', 'password', 'secret'];
62
+ if (sensitiveFields.some(f => key.toLowerCase().includes(f.toLowerCase()))) {
63
+ sanitized[key] = typeof value === 'string' ? '[REDACTED]' : value;
64
+ }
65
+ else {
66
+ sanitized[key] = sanitize(value);
67
+ }
68
+ }
69
+ return sanitized;
70
+ }
71
+ return data;
72
+ }
73
+ // ============================================================================
74
+ // LOGGER IMPLEMENTATION
75
+ // ============================================================================
76
+ /**
77
+ * Create a logger instance.
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * const logger = createLogger({ debug: true });
82
+ * logger.debug('Preparing request', { use_case: 'GetPhoneNumber' });
83
+ * logger.error('Request failed', { code: 'NETWORK_ERROR' });
84
+ * ```
85
+ */
86
+ export function createLogger(config = {}) {
87
+ const { debug = false, prefix = '[PhoneAuth]' } = config;
88
+ /**
89
+ * Format a log message.
90
+ */
91
+ function format(message, data) {
92
+ const formattedMessage = `${prefix} ${message}`;
93
+ if (data !== undefined) {
94
+ const sanitizedData = sanitize(data);
95
+ return [formattedMessage, sanitizedData];
96
+ }
97
+ return [formattedMessage];
98
+ }
99
+ return {
100
+ debug(message, data) {
101
+ if (debug) {
102
+ const args = format(message, data);
103
+ console.debug(...args);
104
+ }
105
+ },
106
+ info(message, data) {
107
+ const args = format(message, data);
108
+ console.info(...args);
109
+ },
110
+ warn(message, data) {
111
+ const args = format(message, data);
112
+ console.warn(...args);
113
+ },
114
+ error(message, data) {
115
+ const args = format(message, data);
116
+ console.error(...args);
117
+ },
118
+ };
119
+ }
120
+ /**
121
+ * Create a no-op logger (silent).
122
+ * Useful for testing or when logging is not needed.
123
+ */
124
+ export function createNoopLogger() {
125
+ return {
126
+ debug: () => { },
127
+ info: () => { },
128
+ warn: () => { },
129
+ error: () => { },
130
+ };
131
+ }
@@ -0,0 +1,424 @@
1
+ /**
2
+ * Phone Authentication Client
3
+ *
4
+ * Main SDK client for phone number authentication.
5
+ * Supports all strategies: TS43 (Android), Link (iOS), Desktop (QR).
6
+ *
7
+ * IMPORTANT: invokeSecurePrompt must be called from a user gesture (button click)
8
+ * because both iOS App Clips and Android Digital Credentials API require
9
+ * transient activation (user interaction) to open system UI.
10
+ */
11
+ import { USE_CASE, AUTHENTICATION_STRATEGY, validatePhoneNumber, validatePlmn, } from '../core';
12
+ import { createAuthError, ERROR_CODES, isAuthError } from '../core/errors';
13
+ import { createHttpClient } from './http';
14
+ import { createLogger, createNoopLogger } from './logger';
15
+ import { createPollingHandler } from './strategies/polling';
16
+ import { AuthModal, createQRCodeDataFromDesktop } from '../ui/modal';
17
+ import { MobileDebugConsole } from '../ui/mobile-debug-console';
18
+ export class PhoneAuthClient {
19
+ constructor(config = {}) {
20
+ // Build complete config with defaults
21
+ this.config = {
22
+ ...config,
23
+ endpoints: {
24
+ prepare: config.endpoints?.prepare || '/api/magic-auth/prepare',
25
+ process: config.endpoints?.process || '/api/magic-auth/process',
26
+ polling: config.endpoints?.polling,
27
+ },
28
+ timeout: config.timeout || 30000,
29
+ pollingInterval: config.pollingInterval || 2000,
30
+ maxPollingAttempts: config.maxPollingAttempts || 30,
31
+ debug: config.debug || false,
32
+ };
33
+ // Use custom or default HTTP client
34
+ this.http = config.httpClient || createHttpClient({
35
+ timeout: this.config.timeout,
36
+ headers: config.headers,
37
+ dynamicHeaders: config.dynamicHeaders,
38
+ });
39
+ // Use custom or default logger
40
+ this.logger = config.logger || (this.config.debug
41
+ ? createLogger({ debug: true })
42
+ : createNoopLogger());
43
+ this.logger.debug('PhoneAuthClient initialized', {
44
+ endpoints: this.config.endpoints,
45
+ timeout: this.config.timeout,
46
+ });
47
+ // Initialize mobile debug console if configured
48
+ if (config.devtools?.showMobileConsole && typeof window !== 'undefined') {
49
+ MobileDebugConsole.init();
50
+ this.logger.info('Mobile debug console enabled');
51
+ }
52
+ }
53
+ // ==========================================================================
54
+ // PUBLIC API - High-Level
55
+ // ==========================================================================
56
+ /**
57
+ * Complete authentication flow in one call.
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * // Get phone number
62
+ * const result = await client.authenticate({ use_case: 'GetPhoneNumber' });
63
+ * console.log(result.phone_number);
64
+ *
65
+ * // Verify phone number
66
+ * const result = await client.authenticate({
67
+ * use_case: 'VerifyPhoneNumber',
68
+ * phone_number: '+14155551234'
69
+ * });
70
+ * console.log(result.verified);
71
+ * ```
72
+ */
73
+ async authenticate(request, options) {
74
+ // Step 1: Prepare
75
+ const prepared = await this.prepare(request);
76
+ // Step 2: Invoke
77
+ const invokeResult = await this.invokeSecurePrompt(prepared, options);
78
+ // Step 3: Wait for credential and process
79
+ const credential = await invokeResult.credential;
80
+ // Determine use_case from (in priority order):
81
+ // 1. session.use_case (direct field)
82
+ // 2. session.metadata.use_case (server returns it in metadata)
83
+ // 3. request.use_case (fallback to original request)
84
+ const useCase = prepared.session.use_case
85
+ || prepared.session.metadata?.use_case
86
+ || request.use_case;
87
+ if (!useCase) {
88
+ throw createAuthError(ERROR_CODES.INVALID_RESPONSE, 'Could not determine use_case from session or request');
89
+ }
90
+ if (useCase === USE_CASE.VERIFY_PHONE_NUMBER) {
91
+ return this.verifyPhoneNumber(credential, prepared.session);
92
+ }
93
+ else {
94
+ return this.getPhoneNumber(credential, prepared.session);
95
+ }
96
+ }
97
+ // ==========================================================================
98
+ // PUBLIC API - Granular Steps
99
+ // ==========================================================================
100
+ /**
101
+ * Step 1: Prepare authentication request.
102
+ *
103
+ * Validates input and sends prepare request to backend.
104
+ */
105
+ async prepare(request) {
106
+ this.logger.debug('Preparing authentication', { use_case: request.use_case });
107
+ // Validate phone number if provided
108
+ if (request.phone_number) {
109
+ const phoneValidation = validatePhoneNumber(request.phone_number);
110
+ if (!phoneValidation.valid) {
111
+ throw createAuthError(ERROR_CODES.INVALID_PHONE_NUMBER, phoneValidation.error);
112
+ }
113
+ }
114
+ // Validate PLMN if provided (deprecated)
115
+ if (request.plmn) {
116
+ const plmnValidation = validatePlmn(request.plmn);
117
+ if (!plmnValidation.valid) {
118
+ throw createAuthError(ERROR_CODES.INVALID_PLMN, plmnValidation.error);
119
+ }
120
+ }
121
+ // Validate use_case is provided (unless parent_session_id is given)
122
+ if (!request.use_case && !request.options?.parent_session_id) {
123
+ throw createAuthError(ERROR_CODES.MISSING_PARAMETERS, 'use_case is required');
124
+ }
125
+ // Build request with client info
126
+ const requestBody = {
127
+ ...request,
128
+ id: request.id || this.generateRequestId(),
129
+ client_info: request.client_info || {
130
+ user_agent: navigator.userAgent,
131
+ platform: navigator.platform,
132
+ },
133
+ };
134
+ try {
135
+ const response = await this.http.post(this.config.endpoints.prepare, requestBody);
136
+ this.logger.debug('Prepare successful', {
137
+ strategy: response.authentication_strategy,
138
+ sessionKey: response.session.session_key,
139
+ });
140
+ return response;
141
+ }
142
+ catch (error) {
143
+ this.logger.error('Prepare failed', error);
144
+ throw error;
145
+ }
146
+ }
147
+ /**
148
+ * Step 2: Invoke secure prompt for authentication.
149
+ *
150
+ * IMPORTANT: Must be called from a user gesture (button click).
151
+ *
152
+ * Returns unified InvokeResult with:
153
+ * - credential: Promise that resolves to AuthCredential
154
+ * - cancel?: Function to cancel (Link/Desktop only)
155
+ * - strategy: The authentication strategy used
156
+ * - session: Session info
157
+ */
158
+ async invokeSecurePrompt(prepared, options) {
159
+ const { authentication_strategy: strategy, session, data } = prepared;
160
+ this.logger.debug('Invoking secure prompt', { strategy });
161
+ switch (strategy) {
162
+ case AUTHENTICATION_STRATEGY.TS43:
163
+ return this.handleTS43(data, session);
164
+ case AUTHENTICATION_STRATEGY.LINK:
165
+ return this.handleLink(data, session, options);
166
+ case AUTHENTICATION_STRATEGY.DESKTOP:
167
+ return this.handleDesktop(data, session, options);
168
+ default:
169
+ throw createAuthError(ERROR_CODES.UNSUPPORTED_STRATEGY, `Unknown strategy: ${strategy}`);
170
+ }
171
+ }
172
+ /**
173
+ * Step 3A: Get phone number from credential.
174
+ */
175
+ async getPhoneNumber(credential, session) {
176
+ this.logger.debug('Getting phone number');
177
+ const requestBody = {
178
+ session,
179
+ credential,
180
+ use_case: USE_CASE.GET_PHONE_NUMBER,
181
+ };
182
+ try {
183
+ const response = await this.http.post(this.config.endpoints.process, requestBody);
184
+ this.logger.info('Phone number retrieved');
185
+ return response;
186
+ }
187
+ catch (error) {
188
+ this.logger.error('Get phone number failed', error);
189
+ throw error;
190
+ }
191
+ }
192
+ /**
193
+ * Step 3B: Verify phone number with credential.
194
+ */
195
+ async verifyPhoneNumber(credential, session) {
196
+ this.logger.debug('Verifying phone number');
197
+ const requestBody = {
198
+ session,
199
+ credential,
200
+ use_case: USE_CASE.VERIFY_PHONE_NUMBER,
201
+ };
202
+ try {
203
+ const response = await this.http.post(this.config.endpoints.process, requestBody);
204
+ this.logger.info('Phone number verified', { verified: response.verified });
205
+ return response;
206
+ }
207
+ catch (error) {
208
+ this.logger.error('Verify phone number failed', error);
209
+ throw error;
210
+ }
211
+ }
212
+ // ==========================================================================
213
+ // PUBLIC API - Utilities
214
+ // ==========================================================================
215
+ /**
216
+ * Check if Digital Credentials API is supported.
217
+ */
218
+ isSupported() {
219
+ if (typeof window === 'undefined')
220
+ return false;
221
+ return 'DigitalCredential' in window;
222
+ }
223
+ /**
224
+ * Get browser support info for debugging.
225
+ */
226
+ getBrowserSupportInfo() {
227
+ if (typeof window === 'undefined') {
228
+ return { supported: false, browser: 'unknown', message: 'Not in browser' };
229
+ }
230
+ const userAgent = navigator.userAgent;
231
+ const isChrome = /Chrome/.test(userAgent) && /Google Inc/.test(navigator.vendor);
232
+ const isEdge = /Edg\//.test(userAgent);
233
+ const supported = this.isSupported();
234
+ if (supported) {
235
+ return {
236
+ supported: true,
237
+ browser: isChrome ? 'Chrome' : isEdge ? 'Edge' : 'Other',
238
+ };
239
+ }
240
+ return {
241
+ supported: false,
242
+ browser: isChrome ? 'Chrome' : isEdge ? 'Edge' : 'Other',
243
+ message: 'Enable chrome://flags/#web-identity-digital-credentials',
244
+ };
245
+ }
246
+ // ==========================================================================
247
+ // PRIVATE - Strategy Handlers
248
+ // ==========================================================================
249
+ /**
250
+ * Handle TS43 strategy (Android Digital Credentials API).
251
+ */
252
+ async handleTS43(data, session) {
253
+ // Check browser support
254
+ if (!this.isSupported()) {
255
+ throw createAuthError(ERROR_CODES.BROWSER_NOT_SUPPORTED, 'Digital Credentials API not available');
256
+ }
257
+ // Build credential request
258
+ const credentialRequest = {
259
+ digital: {
260
+ requests: [{
261
+ protocol: data.protocol,
262
+ data: data.data,
263
+ }],
264
+ },
265
+ };
266
+ this.logger.debug('Invoking Digital Credentials API');
267
+ // Create credential promise - resolves to just the credential string
268
+ const credentialPromise = (async () => {
269
+ try {
270
+ const response = await navigator.credentials.get(credentialRequest);
271
+ const digitalResponse = response;
272
+ if (!digitalResponse?.data?.vp_token) {
273
+ throw createAuthError(ERROR_CODES.INVALID_RESPONSE, 'No credential returned from Digital Credentials API');
274
+ }
275
+ // Extract credential from vp_token
276
+ const vpToken = digitalResponse.data.vp_token;
277
+ const credentialValue = typeof vpToken === 'string'
278
+ ? vpToken
279
+ : Object.values(vpToken)[0];
280
+ const credential = Array.isArray(credentialValue)
281
+ ? credentialValue[0]
282
+ : credentialValue;
283
+ // Validate credential is not empty/undefined
284
+ if (!credential || (typeof credential === 'string' && credential.trim() === '')) {
285
+ throw createAuthError(ERROR_CODES.INVALID_RESPONSE, 'Empty credential returned from Digital Credentials API');
286
+ }
287
+ // Return just the credential string
288
+ return credential;
289
+ }
290
+ catch (error) {
291
+ // If it's already an AuthError, rethrow it directly
292
+ if (isAuthError(error)) {
293
+ throw error;
294
+ }
295
+ // Map browser errors to SDK errors
296
+ const err = error;
297
+ if (err.name === 'NotAllowedError' ||
298
+ (err.name === 'NetworkError' && err.code === 19)) {
299
+ throw createAuthError(ERROR_CODES.USER_CANCELLED, 'User cancelled authentication');
300
+ }
301
+ if (err.name === 'NotSupportedError') {
302
+ throw createAuthError(ERROR_CODES.BROWSER_NOT_SUPPORTED, 'Browser not supported');
303
+ }
304
+ throw createAuthError(ERROR_CODES.NETWORK_ERROR, err.message || 'Credential request failed');
305
+ }
306
+ })();
307
+ // TS43 returns immediately (no cancel needed)
308
+ return {
309
+ credential: credentialPromise,
310
+ cancel: undefined,
311
+ strategy: AUTHENTICATION_STRATEGY.TS43,
312
+ session,
313
+ };
314
+ }
315
+ /**
316
+ * Handle Link strategy (iOS App Clips).
317
+ * Uses window.location.href to navigate to the App Clip.
318
+ */
319
+ handleLink(data, session, options) {
320
+ if (!data.url) {
321
+ throw createAuthError(ERROR_CODES.INVALID_RESPONSE, 'Missing link URL');
322
+ }
323
+ this.logger.debug('Opening App Clip', { url: data.url });
324
+ // Navigate to App Clip URL (must be from user gesture)
325
+ // Using window.location.href for better iOS compatibility
326
+ window.location.href = data.url;
327
+ // Start polling for authentication status
328
+ const polling = createPollingHandler({
329
+ sessionKey: session.session_key,
330
+ interval: options?.pollingInterval || this.config.pollingInterval,
331
+ maxAttempts: options?.maxPollingAttempts || this.config.maxPollingAttempts,
332
+ pollingEndpoint: options?.pollingEndpoint || this.config.endpoints.polling,
333
+ statusUrl: data.status_url,
334
+ logger: this.logger,
335
+ });
336
+ // Create credential promise from polling - resolves to just the credential string
337
+ const credentialPromise = polling.start().then(result => {
338
+ if (result.status === 'completed' && result.credential) {
339
+ return result.credential;
340
+ }
341
+ throw createAuthError(ERROR_CODES.VERIFICATION_FAILED, result.message || 'Link authentication failed');
342
+ });
343
+ return {
344
+ credential: credentialPromise,
345
+ cancel: () => polling.cancel(),
346
+ strategy: AUTHENTICATION_STRATEGY.LINK,
347
+ session,
348
+ };
349
+ }
350
+ /**
351
+ * Handle Desktop strategy (QR code).
352
+ */
353
+ handleDesktop(data, session, options) {
354
+ // Extract session ID for polling
355
+ const sessionId = data.data?.session_id || session.session_key;
356
+ this.logger.debug('Starting desktop authentication', { sessionId });
357
+ // Start polling for authentication status
358
+ const polling = createPollingHandler({
359
+ sessionKey: sessionId,
360
+ interval: options?.pollingInterval || this.config.pollingInterval,
361
+ maxAttempts: options?.maxPollingAttempts || this.config.maxPollingAttempts,
362
+ pollingEndpoint: options?.pollingEndpoint || this.config.endpoints.polling,
363
+ statusUrl: data.data?.status_url,
364
+ logger: this.logger,
365
+ });
366
+ // Show modal UI if not prevented
367
+ let modal = null;
368
+ if (!options?.preventDefaultUI) {
369
+ try {
370
+ // Create QR code data from DesktopData
371
+ const qrData = createQRCodeDataFromDesktop(data);
372
+ // Create and show modal
373
+ modal = new AuthModal(options?.modalOptions);
374
+ modal.showQRCode(qrData, options?.modalOptions?.description || 'Scan with your phone camera');
375
+ // Set close callback to cancel polling
376
+ modal.setCloseCallback(() => {
377
+ this.logger.debug('Modal closed by user, cancelling authentication');
378
+ polling.cancel();
379
+ });
380
+ this.logger.debug('Desktop modal displayed');
381
+ }
382
+ catch (e) {
383
+ this.logger.warn('Failed to show modal, continuing with polling only', e);
384
+ }
385
+ }
386
+ // Create credential promise from polling - resolves to just the credential string
387
+ const credentialPromise = polling.start().then(result => {
388
+ // Close modal on success
389
+ if (modal) {
390
+ modal.close();
391
+ }
392
+ if (result.status === 'completed' && result.credential) {
393
+ return result.credential;
394
+ }
395
+ throw createAuthError(ERROR_CODES.VERIFICATION_FAILED, result.message || 'Desktop authentication failed');
396
+ }).catch(error => {
397
+ // Close modal on error
398
+ if (modal) {
399
+ modal.close();
400
+ }
401
+ throw error;
402
+ });
403
+ return {
404
+ credential: credentialPromise,
405
+ cancel: () => {
406
+ polling.cancel();
407
+ if (modal) {
408
+ modal.close();
409
+ }
410
+ },
411
+ strategy: AUTHENTICATION_STRATEGY.DESKTOP,
412
+ session,
413
+ };
414
+ }
415
+ // ==========================================================================
416
+ // PRIVATE - Utilities
417
+ // ==========================================================================
418
+ /**
419
+ * Generate a unique request ID.
420
+ */
421
+ generateRequestId() {
422
+ return `web-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
423
+ }
424
+ }