@glideidentity/web-client-sdk 6.0.0-beta.4 → 6.0.0

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 (41) hide show
  1. package/dist/browser/web-client-sdk.min.js +1 -1
  2. package/dist/cjs/adapters/react.js +10 -1
  3. package/dist/cjs/adapters/vue.js +10 -1
  4. package/dist/cjs/client/http.js +5 -1
  5. package/dist/cjs/client/logger.js +1 -7
  6. package/dist/cjs/client/phone-auth-client.js +35 -2
  7. package/dist/cjs/core/index.js +1 -2
  8. package/dist/cjs/core/type-guards.js +16 -4
  9. package/dist/cjs/core/validators.js +0 -29
  10. package/dist/cjs/index.js +1 -2
  11. package/dist/cjs/test/fixtures.js +273 -0
  12. package/dist/cjs/test/setup.js +117 -0
  13. package/dist/esm/adapters/react.js +10 -1
  14. package/dist/esm/adapters/vue.js +10 -1
  15. package/dist/esm/client/http.js +5 -1
  16. package/dist/esm/client/logger.js +1 -7
  17. package/dist/esm/client/phone-auth-client.js +36 -3
  18. package/dist/esm/core/index.js +1 -1
  19. package/dist/esm/core/type-guards.js +16 -4
  20. package/dist/esm/core/validators.js +0 -28
  21. package/dist/esm/index.js +1 -1
  22. package/dist/esm/test/fixtures.js +270 -0
  23. package/dist/esm/test/setup.js +110 -0
  24. package/dist/types/adapters/react.d.ts.map +1 -1
  25. package/dist/types/adapters/vue.d.ts.map +1 -1
  26. package/dist/types/client/http.d.ts.map +1 -1
  27. package/dist/types/client/logger.d.ts.map +1 -1
  28. package/dist/types/client/phone-auth-client.d.ts.map +1 -1
  29. package/dist/types/core/index.d.ts +1 -1
  30. package/dist/types/core/index.d.ts.map +1 -1
  31. package/dist/types/core/type-guards.d.ts +3 -0
  32. package/dist/types/core/type-guards.d.ts.map +1 -1
  33. package/dist/types/core/validators.d.ts +0 -7
  34. package/dist/types/core/validators.d.ts.map +1 -1
  35. package/dist/types/index.d.ts +1 -1
  36. package/dist/types/index.d.ts.map +1 -1
  37. package/dist/types/test/fixtures.d.ts +178 -0
  38. package/dist/types/test/fixtures.d.ts.map +1 -0
  39. package/dist/types/test/setup.d.ts +39 -0
  40. package/dist/types/test/setup.d.ts.map +1 -0
  41. package/package.json +1 -1
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+ /**
3
+ * Test Fixtures - Mock Data for Unit Tests
4
+ *
5
+ * Contains valid and invalid mock data for all SDK types.
6
+ * Based on TypeScript type definitions - may need adjustment
7
+ * to match actual backend responses.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.sessionKeys = exports.validPlmn = exports.invalidPlmn = exports.validPhoneNumbers = exports.invalidPhoneNumbers = exports.invalidInvokeResultMissingSession = exports.invalidInvokeResultMissingStrategy = exports.invalidInvokeResultMissingCredential = exports.ambiguousNestedData = exports.misleadingDataLooksLikeTS43 = exports.backendErrorAsNull = exports.backendErrorAsArray = exports.backendErrorAsString = exports.backendErrorMinimal = exports.backendErrorWithError = exports.backendErrorWithCode = exports.pollingStatusError = exports.pollingStatusExpired = exports.pollingStatusCompleted = exports.pollingStatusPending = exports.validVerifyPhoneNumberResponse = exports.validGetPhoneNumberResponse = exports.validAuthCredential = exports.cancellableInvokeResult = exports.validInvokeResult = exports.validDesktopResponse = exports.minimalDesktopData = exports.validDesktopData = exports.validLinkResponse = exports.minimalLinkData = exports.validLinkData = exports.validTS43Response = exports.validTS43Data = exports.minimalSession = exports.validSession = void 0;
11
+ const types_1 = require("../core/types");
12
+ // ============================================================================
13
+ // SESSION FIXTURES
14
+ // ============================================================================
15
+ exports.validSession = {
16
+ session_key: 'test-session-key-12345678',
17
+ use_case: types_1.USE_CASE.GET_PHONE_NUMBER,
18
+ nonce: 'abc123nonce',
19
+ metadata: { request_id: 'req-123' },
20
+ };
21
+ exports.minimalSession = {
22
+ session_key: 'minimal-session-1234567',
23
+ };
24
+ // ============================================================================
25
+ // TS43 STRATEGY FIXTURES
26
+ // ============================================================================
27
+ exports.validTS43Data = {
28
+ protocol: 'openid4vp-v1-unsigned',
29
+ data: {
30
+ nonce: 'ts43-nonce-value-12345',
31
+ response_mode: 'direct_post',
32
+ response_type: 'vp_token',
33
+ dcql_query: {
34
+ credentials: [
35
+ {
36
+ id: 'phone-credential',
37
+ format: 'vc+sd-jwt',
38
+ meta: {
39
+ vct_values: ['urn:glide:phone-number:v1'],
40
+ // Fake JWT structure for testing - NOT a real token
41
+ credential_authorization_jwt: 'eyJGQUtFIjoiVEVTVCJ9.eyJGQUtFIjoiVEVTVCJ9.FAKE',
42
+ },
43
+ claims: ['phone_number'],
44
+ },
45
+ ],
46
+ },
47
+ },
48
+ };
49
+ exports.validTS43Response = {
50
+ authentication_strategy: types_1.AUTHENTICATION_STRATEGY.TS43,
51
+ session: exports.validSession,
52
+ data: exports.validTS43Data,
53
+ };
54
+ // ============================================================================
55
+ // LINK STRATEGY FIXTURES
56
+ // ============================================================================
57
+ exports.validLinkData = {
58
+ url: 'https://appclip.glideidentity.app/auth?session=abc123',
59
+ return_url: 'https://myapp.com/callback',
60
+ status_url: 'https://api.glideidentity.app/public/status/abc123',
61
+ params: { theme: 'dark' },
62
+ };
63
+ exports.minimalLinkData = {
64
+ url: 'https://appclip.glideidentity.app/auth',
65
+ };
66
+ exports.validLinkResponse = {
67
+ authentication_strategy: types_1.AUTHENTICATION_STRATEGY.LINK,
68
+ session: exports.validSession,
69
+ data: exports.validLinkData,
70
+ };
71
+ // ============================================================================
72
+ // DESKTOP STRATEGY FIXTURES
73
+ // ============================================================================
74
+ exports.validDesktopData = {
75
+ protocol: 'qr-auth-v1',
76
+ data: {
77
+ qr_code_image: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...',
78
+ ios_qr_image: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUB...',
79
+ android_qr_image: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUC...',
80
+ ios_url: 'https://appclip.glideidentity.app/ios?session=abc123',
81
+ android_url: 'https://glideidentity.app/android?session=abc123',
82
+ session_id: 'desktop-session-abc123',
83
+ status_url: 'https://api.glideidentity.app/public/status/desktop-session-abc123',
84
+ url: 'https://glideidentity.app/mobile?session=abc123',
85
+ polling_interval: 2000,
86
+ expires_in: 300,
87
+ },
88
+ };
89
+ exports.minimalDesktopData = {
90
+ data: {
91
+ status_url: 'https://api.glideidentity.app/public/status/minimal123',
92
+ },
93
+ };
94
+ exports.validDesktopResponse = {
95
+ authentication_strategy: types_1.AUTHENTICATION_STRATEGY.DESKTOP,
96
+ session: exports.validSession,
97
+ data: exports.validDesktopData,
98
+ };
99
+ // ============================================================================
100
+ // INVOKE RESULT FIXTURES
101
+ // ============================================================================
102
+ exports.validInvokeResult = {
103
+ credential: Promise.resolve('mock-credential-token-abc123'),
104
+ strategy: types_1.AUTHENTICATION_STRATEGY.TS43,
105
+ session: exports.validSession,
106
+ };
107
+ exports.cancellableInvokeResult = {
108
+ credential: Promise.resolve('mock-credential-token-abc123'),
109
+ strategy: types_1.AUTHENTICATION_STRATEGY.DESKTOP,
110
+ session: exports.validSession,
111
+ cancel: jest.fn(),
112
+ };
113
+ // ============================================================================
114
+ // AUTH CREDENTIAL FIXTURES
115
+ // ============================================================================
116
+ exports.validAuthCredential = {
117
+ credential: 'mock-credential-token-xyz789',
118
+ phone_number: '+14155551234',
119
+ session: exports.validSession,
120
+ authenticated: true,
121
+ };
122
+ // ============================================================================
123
+ // PROCESS RESPONSE FIXTURES
124
+ // ============================================================================
125
+ exports.validGetPhoneNumberResponse = {
126
+ phone_number: '+14155551234',
127
+ aud: 'carrier-audience-id',
128
+ };
129
+ exports.validVerifyPhoneNumberResponse = {
130
+ phone_number: '+14155551234',
131
+ verified: true,
132
+ aud: 'carrier-audience-id',
133
+ };
134
+ // ============================================================================
135
+ // POLLING STATUS FIXTURES
136
+ // ============================================================================
137
+ exports.pollingStatusPending = {
138
+ status: 'pending',
139
+ message: 'Waiting for user authentication',
140
+ };
141
+ exports.pollingStatusCompleted = {
142
+ status: 'completed',
143
+ credential: 'completed-credential-token-123',
144
+ session: exports.validSession,
145
+ };
146
+ exports.pollingStatusExpired = {
147
+ status: 'expired',
148
+ message: 'Session has expired',
149
+ };
150
+ exports.pollingStatusError = {
151
+ status: 'error',
152
+ message: 'Authentication failed',
153
+ code: 'AUTH_FAILED',
154
+ };
155
+ // ============================================================================
156
+ // BACKEND ERROR FIXTURES
157
+ // ============================================================================
158
+ exports.backendErrorWithCode = {
159
+ code: 'INVALID_SESSION',
160
+ message: 'Session not found or expired',
161
+ status: 400,
162
+ request_id: 'req-error-123',
163
+ trace_id: 'trace-abc123',
164
+ span_id: 'span-xyz789',
165
+ service: 'magic-auth',
166
+ };
167
+ exports.backendErrorWithError = {
168
+ error: 'RATE_LIMITED',
169
+ message: 'Too many requests',
170
+ status: 429,
171
+ details: { retryAfter: 60 },
172
+ };
173
+ exports.backendErrorMinimal = {
174
+ status: 500,
175
+ };
176
+ exports.backendErrorAsString = 'Internal Server Error';
177
+ exports.backendErrorAsArray = ['error1', 'error2'];
178
+ exports.backendErrorAsNull = null;
179
+ // ============================================================================
180
+ // INVALID/MISLEADING DATA FIXTURES
181
+ // ============================================================================
182
+ /**
183
+ * Data that looks like TS43Data but is actually DesktopData.
184
+ * Used to test type guard discrimination.
185
+ */
186
+ exports.misleadingDataLooksLikeTS43 = {
187
+ protocol: 'some-protocol',
188
+ data: {
189
+ // Has nested data object like TS43, but no dcql_query
190
+ nonce: 'fake-nonce',
191
+ session_id: 'session-123',
192
+ status_url: 'https://example.com/status',
193
+ },
194
+ };
195
+ /**
196
+ * Data that has nested data object (could be TS43 or Desktop).
197
+ * Should be correctly identified by type guards.
198
+ */
199
+ exports.ambiguousNestedData = {
200
+ data: {
201
+ something: 'value',
202
+ },
203
+ };
204
+ /**
205
+ * Invalid InvokeResult - missing required fields.
206
+ */
207
+ exports.invalidInvokeResultMissingCredential = {
208
+ strategy: types_1.AUTHENTICATION_STRATEGY.TS43,
209
+ session: exports.validSession,
210
+ };
211
+ exports.invalidInvokeResultMissingStrategy = {
212
+ credential: Promise.resolve('token'),
213
+ session: exports.validSession,
214
+ };
215
+ exports.invalidInvokeResultMissingSession = {
216
+ credential: Promise.resolve('token'),
217
+ strategy: types_1.AUTHENTICATION_STRATEGY.TS43,
218
+ };
219
+ /**
220
+ * Invalid phone numbers for validation tests.
221
+ */
222
+ exports.invalidPhoneNumbers = {
223
+ missingPlus: '14155551234',
224
+ tooShort: '+123456',
225
+ tooLong: '+12345678901234567',
226
+ withSpaces: '+1 415 555 1234',
227
+ withDashes: '+1-415-555-1234',
228
+ withParens: '+1(415)5551234',
229
+ withLetters: '+1415ABC1234',
230
+ startsWithZero: '+0123456789',
231
+ empty: '',
232
+ };
233
+ /**
234
+ * Valid phone numbers for validation tests.
235
+ */
236
+ exports.validPhoneNumbers = {
237
+ us: '+14155551234',
238
+ uk: '+447911123456',
239
+ india: '+919876543210',
240
+ germany: '+4915112345678',
241
+ minimum: '+1234567', // 8 chars total
242
+ maximum: '+123456789012345', // 16 chars total (+ plus 15 digits)
243
+ };
244
+ /**
245
+ * Invalid PLMN values for validation tests.
246
+ */
247
+ exports.invalidPlmn = {
248
+ mccTooShort: { mcc: '31', mnc: '260' },
249
+ mccTooLong: { mcc: '3100', mnc: '260' },
250
+ mncTooShort: { mcc: '310', mnc: '2' },
251
+ mncTooLong: { mcc: '310', mnc: '2600' },
252
+ mccWithLetters: { mcc: 'ABC', mnc: '260' },
253
+ mncWithLetters: { mcc: '310', mnc: 'AB' },
254
+ };
255
+ /**
256
+ * Valid PLMN values for validation tests.
257
+ */
258
+ exports.validPlmn = {
259
+ att: { mcc: '310', mnc: '410' },
260
+ verizon: { mcc: '311', mnc: '480' },
261
+ tmobile: { mcc: '310', mnc: '260' },
262
+ twoDigitMnc: { mcc: '234', mnc: '15' },
263
+ threeDigitMnc: { mcc: '234', mnc: '150' },
264
+ };
265
+ /**
266
+ * Session keys for validation tests.
267
+ */
268
+ exports.sessionKeys = {
269
+ valid: 'valid-session-key-1234567890',
270
+ tooShort: 'short-key',
271
+ minimum: '1234567890123456', // exactly 16 chars
272
+ empty: '',
273
+ };
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ /**
3
+ * Jest Test Setup
4
+ *
5
+ * Global setup for all unit tests including:
6
+ * - Jest DOM matchers
7
+ * - Global mocks
8
+ * - Test utilities
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.createMockResponse = createMockResponse;
12
+ exports.flushPromises = flushPromises;
13
+ exports.advanceTimersAndFlush = advanceTimersAndFlush;
14
+ exports.createConsoleMocks = createConsoleMocks;
15
+ exports.restoreConsoleMocks = restoreConsoleMocks;
16
+ require("@testing-library/jest-dom");
17
+ // ============================================================================
18
+ // GLOBAL MOCKS
19
+ // ============================================================================
20
+ /**
21
+ * Mock fetch globally for HTTP tests.
22
+ * Individual tests can override this.
23
+ */
24
+ global.fetch = jest.fn();
25
+ /**
26
+ * Mock AbortController for timeout tests.
27
+ */
28
+ global.AbortController = class MockAbortController {
29
+ constructor() {
30
+ this.signal = {
31
+ aborted: false,
32
+ addEventListener: jest.fn(),
33
+ removeEventListener: jest.fn(),
34
+ onabort: null,
35
+ reason: undefined,
36
+ throwIfAborted: jest.fn(),
37
+ dispatchEvent: jest.fn(),
38
+ };
39
+ this.abort = jest.fn(() => {
40
+ this.signal.aborted = true;
41
+ });
42
+ }
43
+ };
44
+ // ============================================================================
45
+ // TEST UTILITIES
46
+ // ============================================================================
47
+ /**
48
+ * Helper to create a mock Response object.
49
+ */
50
+ function createMockResponse(body, options = {}) {
51
+ const { status = 200, ok = status >= 200 && status < 300, headers = {} } = options;
52
+ return {
53
+ ok,
54
+ status,
55
+ statusText: status === 200 ? 'OK' : 'Error',
56
+ headers: new Headers(headers),
57
+ json: jest.fn().mockResolvedValue(body),
58
+ text: jest.fn().mockResolvedValue(JSON.stringify(body)),
59
+ blob: jest.fn(),
60
+ arrayBuffer: jest.fn(),
61
+ formData: jest.fn(),
62
+ clone: jest.fn(),
63
+ body: null,
64
+ bodyUsed: false,
65
+ redirected: false,
66
+ type: 'basic',
67
+ url: '',
68
+ bytes: jest.fn(),
69
+ };
70
+ }
71
+ /**
72
+ * Helper to wait for async operations.
73
+ */
74
+ function flushPromises() {
75
+ return new Promise(resolve => setImmediate(resolve));
76
+ }
77
+ /**
78
+ * Helper to advance timers and flush promises.
79
+ */
80
+ async function advanceTimersAndFlush(ms) {
81
+ jest.advanceTimersByTime(ms);
82
+ await flushPromises();
83
+ }
84
+ // ============================================================================
85
+ // CONSOLE MOCK HELPERS
86
+ // ============================================================================
87
+ /**
88
+ * Create spies on console methods for logger tests.
89
+ */
90
+ function createConsoleMocks() {
91
+ return {
92
+ debug: jest.spyOn(console, 'debug').mockImplementation(),
93
+ info: jest.spyOn(console, 'info').mockImplementation(),
94
+ warn: jest.spyOn(console, 'warn').mockImplementation(),
95
+ error: jest.spyOn(console, 'error').mockImplementation(),
96
+ };
97
+ }
98
+ /**
99
+ * Restore console mocks.
100
+ */
101
+ function restoreConsoleMocks(mocks) {
102
+ mocks.debug.mockRestore();
103
+ mocks.info.mockRestore();
104
+ mocks.warn.mockRestore();
105
+ mocks.error.mockRestore();
106
+ }
107
+ // ============================================================================
108
+ // CLEANUP
109
+ // ============================================================================
110
+ beforeEach(() => {
111
+ // Clear all mocks before each test
112
+ jest.clearAllMocks();
113
+ });
114
+ afterEach(() => {
115
+ // Reset fetch mock
116
+ global.fetch.mockReset();
117
+ });
@@ -108,9 +108,13 @@ export function usePhoneAuth(config) {
108
108
  * Granular: Invoke.
109
109
  */
110
110
  const invokeSecurePrompt = useCallback(async (prepared, options) => {
111
+ setIsLoading(true);
112
+ setError(null);
111
113
  setStep('invoking');
112
114
  try {
113
- return await client.invokeSecurePrompt(prepared, options);
115
+ const result = await client.invokeSecurePrompt(prepared, options);
116
+ setStep('idle'); // Reset step on success (invoke is intermediate step)
117
+ return result;
114
118
  }
115
119
  catch (err) {
116
120
  const authError = err;
@@ -118,12 +122,16 @@ export function usePhoneAuth(config) {
118
122
  setStep('error');
119
123
  throw err;
120
124
  }
125
+ finally {
126
+ setIsLoading(false);
127
+ }
121
128
  }, [client]);
122
129
  /**
123
130
  * Granular: Get phone number.
124
131
  */
125
132
  const getPhoneNumber = useCallback(async (credential, session) => {
126
133
  setIsLoading(true);
134
+ setError(null);
127
135
  setStep('processing');
128
136
  try {
129
137
  const authResult = await client.getPhoneNumber(credential, session);
@@ -146,6 +154,7 @@ export function usePhoneAuth(config) {
146
154
  */
147
155
  const verifyPhoneNumber = useCallback(async (credential, session) => {
148
156
  setIsLoading(true);
157
+ setError(null);
149
158
  setStep('processing');
150
159
  try {
151
160
  const authResult = await client.verifyPhoneNumber(credential, session);
@@ -99,21 +99,29 @@ export function usePhoneAuth(config) {
99
99
  * Granular: Invoke.
100
100
  */
101
101
  async function invokeSecurePrompt(prepared, options) {
102
+ isLoading.value = true;
103
+ error.value = null;
102
104
  step.value = 'invoking';
103
105
  try {
104
- return await client.invokeSecurePrompt(prepared, options);
106
+ const invokeResult = await client.invokeSecurePrompt(prepared, options);
107
+ step.value = 'idle'; // Reset step on success (invoke is intermediate step)
108
+ return invokeResult;
105
109
  }
106
110
  catch (err) {
107
111
  error.value = err;
108
112
  step.value = 'error';
109
113
  throw err;
110
114
  }
115
+ finally {
116
+ isLoading.value = false;
117
+ }
111
118
  }
112
119
  /**
113
120
  * Granular: Get phone number.
114
121
  */
115
122
  async function getPhoneNumber(credential, session) {
116
123
  isLoading.value = true;
124
+ error.value = null;
117
125
  step.value = 'processing';
118
126
  try {
119
127
  const authResult = await client.getPhoneNumber(credential, session);
@@ -135,6 +143,7 @@ export function usePhoneAuth(config) {
135
143
  */
136
144
  async function verifyPhoneNumber(credential, session) {
137
145
  isLoading.value = true;
146
+ error.value = null;
138
147
  step.value = 'processing';
139
148
  try {
140
149
  const authResult = await client.verifyPhoneNumber(credential, session);
@@ -114,7 +114,11 @@ export function createHttpClient(config = {}) {
114
114
  }
115
115
  // If response is not ok, parse as error
116
116
  if (!response.ok) {
117
- throw parseBackendError({ ...data, status: response.status });
117
+ // Safely build error object - handle non-object responses (strings, null, arrays)
118
+ const errorData = typeof data === 'object' && data !== null && !Array.isArray(data)
119
+ ? { ...data, status: response.status }
120
+ : { status: response.status, rawResponse: data };
121
+ throw parseBackendError(errorData);
118
122
  }
119
123
  return data;
120
124
  }
@@ -11,12 +11,11 @@
11
11
  // ============================================================================
12
12
  /**
13
13
  * Patterns to detect and sanitize PII.
14
+ * Note: Email pattern removed - this SDK only handles phone authentication.
14
15
  */
15
16
  const PII_PATTERNS = {
16
17
  // Phone numbers: +1234567890 or variations
17
18
  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
19
  // JWT tokens (three base64 segments separated by dots)
21
20
  jwt: /(eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*)/g,
22
21
  // Session keys (long alphanumeric strings)
@@ -38,11 +37,6 @@ function sanitize(data) {
38
37
  const visible = 4;
39
38
  return match.slice(0, 2) + '***' + match.slice(-visible);
40
39
  });
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
40
  // Mask JWTs: eyJ... -> [JWT]
47
41
  sanitized = sanitized.replace(PII_PATTERNS.jwt, '[JWT]');
48
42
  // Mask session keys
@@ -8,7 +8,7 @@
8
8
  * because both iOS App Clips and Android Digital Credentials API require
9
9
  * transient activation (user interaction) to open system UI.
10
10
  */
11
- import { USE_CASE, AUTHENTICATION_STRATEGY, validatePhoneNumber, validatePlmn, } from '../core';
11
+ import { USE_CASE, AUTHENTICATION_STRATEGY, validatePhoneNumber, validatePlmn, validateUseCaseRequirements, } from '../core';
12
12
  import { createAuthError, ERROR_CODES, isAuthError } from '../core/errors';
13
13
  import { createHttpClient } from './http';
14
14
  import { createLogger, createNoopLogger } from './logger';
@@ -30,6 +30,7 @@ export class PhoneAuthClient {
30
30
  pollingInterval: config.pollingInterval || 2000,
31
31
  maxPollingAttempts: config.maxPollingAttempts || 30,
32
32
  debug: config.debug || false,
33
+ devEnv: config.devEnv,
33
34
  };
34
35
  // Use custom or default HTTP client
35
36
  this.http = config.httpClient || createHttpClient({
@@ -44,6 +45,7 @@ export class PhoneAuthClient {
44
45
  this.logger.debug('PhoneAuthClient initialized', {
45
46
  endpoints: this.config.endpoints,
46
47
  timeout: this.config.timeout,
48
+ ...(config.devEnv && { devEnv: config.devEnv }),
47
49
  });
48
50
  // Initialize mobile debug console if configured
49
51
  if (config.devtools?.showMobileConsole && typeof window !== 'undefined') {
@@ -123,6 +125,13 @@ export class PhoneAuthClient {
123
125
  if (!request.use_case && !request.options?.parent_session_id) {
124
126
  throw createAuthError(ERROR_CODES.MISSING_PARAMETERS, 'use_case is required');
125
127
  }
128
+ // Validate use_case requirements (e.g., VerifyPhoneNumber requires phone_number)
129
+ if (request.use_case) {
130
+ const useCaseValidation = validateUseCaseRequirements(request.use_case, request.phone_number, !!request.options?.parent_session_id);
131
+ if (!useCaseValidation.valid) {
132
+ throw createAuthError(ERROR_CODES.MISSING_PARAMETERS, useCaseValidation.error);
133
+ }
134
+ }
126
135
  // Build request with client info
127
136
  const requestBody = {
128
137
  ...request,
@@ -285,6 +294,20 @@ export class PhoneAuthClient {
285
294
  if (!credential || (typeof credential === 'string' && credential.trim() === '')) {
286
295
  throw createAuthError(ERROR_CODES.INVALID_RESPONSE, 'Empty credential returned from Digital Credentials API');
287
296
  }
297
+ // Success haptic feedback
298
+ if (typeof navigator !== 'undefined' && navigator.vibrate) {
299
+ // Add a small delay to ensure the browser has regained focus/visibility
300
+ // after the native bottom sheet closes
301
+ setTimeout(() => {
302
+ try {
303
+ // Double tap pattern: vibrate 80ms, pause 50ms, vibrate 80ms
304
+ navigator.vibrate([80, 50, 80]);
305
+ }
306
+ catch (e) {
307
+ // Ignore vibration errors
308
+ }
309
+ }, 200);
310
+ }
288
311
  // Return just the credential string
289
312
  return credential;
290
313
  }
@@ -325,11 +348,16 @@ export class PhoneAuthClient {
325
348
  // Navigate to App Clip URL (must be from user gesture)
326
349
  // Using window.location.href for better iOS compatibility
327
350
  window.location.href = data.url;
328
- // Build polling headers (merge common + polling-specific)
351
+ // Build polling headers (merge common + polling-specific + devEnv)
329
352
  const pollingHeaders = {
330
353
  ...this.config.headers?.common,
331
354
  ...this.config.headers?.polling,
332
355
  };
356
+ // Add developer header if devEnv is set
357
+ if (this.config.devEnv) {
358
+ pollingHeaders['developer'] = this.config.devEnv;
359
+ this.logger.debug('Adding developer header for polling', { devEnv: this.config.devEnv });
360
+ }
333
361
  // Start polling for authentication status
334
362
  const polling = createPollingHandler({
335
363
  sessionKey: session.session_key,
@@ -361,11 +389,16 @@ export class PhoneAuthClient {
361
389
  // Extract session ID for polling
362
390
  const sessionId = data.data?.session_id || session.session_key;
363
391
  this.logger.debug('Starting desktop authentication', { sessionId });
364
- // Build polling headers (merge common + polling-specific)
392
+ // Build polling headers (merge common + polling-specific + devEnv)
365
393
  const pollingHeaders = {
366
394
  ...this.config.headers?.common,
367
395
  ...this.config.headers?.polling,
368
396
  };
397
+ // Add developer header if devEnv is set
398
+ if (this.config.devEnv) {
399
+ pollingHeaders['developer'] = this.config.devEnv;
400
+ this.logger.debug('Adding developer header for polling', { devEnv: this.config.devEnv });
401
+ }
369
402
  // Start polling for authentication status
370
403
  const polling = createPollingHandler({
371
404
  sessionKey: sessionId,
@@ -29,7 +29,7 @@ export { ERROR_CODES, } from './errors';
29
29
  // ============================================================================
30
30
  // VALIDATORS
31
31
  // ============================================================================
32
- export { validatePhoneNumber, validatePlmn, validateUseCaseRequirements, validateNonce, validateSessionKey, E164_REGEX, } from './validators';
32
+ export { validatePhoneNumber, validatePlmn, validateUseCaseRequirements, validateSessionKey, E164_REGEX, } from './validators';
33
33
  // ============================================================================
34
34
  // TYPE GUARDS
35
35
  // ============================================================================
@@ -128,12 +128,24 @@ export function isLinkData(data) {
128
128
  }
129
129
  /**
130
130
  * Check if prepare response data is DesktopData.
131
+ *
132
+ * Note: Both TS43Data and DesktopData have nested 'data' objects,
133
+ * so we must explicitly exclude TS43Data by checking for absence of 'dcql_query'.
131
134
  */
132
135
  export function isDesktopData(data) {
133
- return (data !== null &&
134
- typeof data === 'object' &&
135
- 'data' in data &&
136
- typeof data.data === 'object');
136
+ if (data === null ||
137
+ typeof data !== 'object' ||
138
+ !('data' in data) ||
139
+ typeof data.data !== 'object' ||
140
+ data.data === null) {
141
+ return false;
142
+ }
143
+ // Exclude TS43Data which also has a nested 'data' object but contains 'dcql_query'
144
+ const nestedData = data.data;
145
+ if ('dcql_query' in nestedData) {
146
+ return false;
147
+ }
148
+ return true;
137
149
  }
138
150
  /**
139
151
  * Get strategy-specific data from PrepareResponse with proper typing.
@@ -106,34 +106,6 @@ export function validateUseCaseRequirements(useCase, phoneNumber, hasParentSessi
106
106
  }
107
107
  return { valid: true };
108
108
  }
109
- /**
110
- * Validates nonce format.
111
- *
112
- * @param nonce - Nonce string to validate
113
- * @returns Validation result
114
- */
115
- export function validateNonce(nonce) {
116
- const base64urlRegex = /^[A-Za-z0-9_-]+$/;
117
- if (!nonce || nonce.length === 0) {
118
- return {
119
- valid: false,
120
- error: 'Nonce is required'
121
- };
122
- }
123
- if (!base64urlRegex.test(nonce)) {
124
- return {
125
- valid: false,
126
- error: 'Nonce must be base64url encoded'
127
- };
128
- }
129
- if (nonce.length < 32 || nonce.length > 128) {
130
- return {
131
- valid: false,
132
- error: 'Nonce must be between 32 and 128 characters'
133
- };
134
- }
135
- return { valid: true };
136
- }
137
109
  /**
138
110
  * Validates session key format.
139
111
  *