@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.
- package/dist/browser/web-client-sdk.min.js +1 -1
- package/dist/cjs/adapters/react.js +10 -1
- package/dist/cjs/adapters/vue.js +10 -1
- package/dist/cjs/client/http.js +5 -1
- package/dist/cjs/client/logger.js +1 -7
- package/dist/cjs/client/phone-auth-client.js +35 -2
- package/dist/cjs/core/index.js +1 -2
- package/dist/cjs/core/type-guards.js +16 -4
- package/dist/cjs/core/validators.js +0 -29
- package/dist/cjs/index.js +1 -2
- package/dist/cjs/test/fixtures.js +273 -0
- package/dist/cjs/test/setup.js +117 -0
- package/dist/esm/adapters/react.js +10 -1
- package/dist/esm/adapters/vue.js +10 -1
- package/dist/esm/client/http.js +5 -1
- package/dist/esm/client/logger.js +1 -7
- package/dist/esm/client/phone-auth-client.js +36 -3
- package/dist/esm/core/index.js +1 -1
- package/dist/esm/core/type-guards.js +16 -4
- package/dist/esm/core/validators.js +0 -28
- package/dist/esm/index.js +1 -1
- package/dist/esm/test/fixtures.js +270 -0
- package/dist/esm/test/setup.js +110 -0
- package/dist/types/adapters/react.d.ts.map +1 -1
- package/dist/types/adapters/vue.d.ts.map +1 -1
- package/dist/types/client/http.d.ts.map +1 -1
- package/dist/types/client/logger.d.ts.map +1 -1
- package/dist/types/client/phone-auth-client.d.ts.map +1 -1
- package/dist/types/core/index.d.ts +1 -1
- package/dist/types/core/index.d.ts.map +1 -1
- package/dist/types/core/type-guards.d.ts +3 -0
- package/dist/types/core/type-guards.d.ts.map +1 -1
- package/dist/types/core/validators.d.ts +0 -7
- package/dist/types/core/validators.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/test/fixtures.d.ts +178 -0
- package/dist/types/test/fixtures.d.ts.map +1 -0
- package/dist/types/test/setup.d.ts +39 -0
- package/dist/types/test/setup.d.ts.map +1 -0
- 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
|
-
|
|
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);
|
package/dist/esm/adapters/vue.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/dist/esm/client/http.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/dist/esm/core/index.js
CHANGED
|
@@ -29,7 +29,7 @@ export { ERROR_CODES, } from './errors';
|
|
|
29
29
|
// ============================================================================
|
|
30
30
|
// VALIDATORS
|
|
31
31
|
// ============================================================================
|
|
32
|
-
export { validatePhoneNumber, validatePlmn, validateUseCaseRequirements,
|
|
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
|
-
|
|
134
|
-
typeof data
|
|
135
|
-
'data' in data
|
|
136
|
-
typeof data.data
|
|
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
|
*
|