@truly-you/trulyyou-web-sdk 0.1.24 → 0.1.26
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/index.d.ts +86 -0
- package/index.js +691 -0
- package/package.json +11 -34
- package/README.md +0 -57
- package/dist/index.d.ts +0 -2
- package/dist/index.esm.js +0 -9
- package/dist/index.esm.js.map +0 -1
- package/dist/index.js +0 -9
- package/dist/index.js.map +0 -1
- package/dist/index.umd.js +0 -9
- package/dist/index.umd.js.map +0 -1
- package/dist/sdk/TrulyYouSDK.d.ts +0 -96
- package/dist/types.d.ts +0 -39
- package/rollup.config.js +0 -58
- package/src/index.ts +0 -3
- package/src/sdk/TrulyYouSDK.ts +0 -2041
- package/src/types.ts +0 -43
- package/tsconfig.json +0 -20
package/index.js
ADDED
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TrulyYou SDK - Strong Customer Authentication
|
|
3
|
+
* Embed via: <script src="https://your-domain.com/truly-you-sdk.js"></script>
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function(window) {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
class TrulyYouSDK {
|
|
10
|
+
constructor(config = {}) {
|
|
11
|
+
this.appId = config.appId;
|
|
12
|
+
this.mode = config.mode || 'visible'; // 'visible' or 'hidden'
|
|
13
|
+
this.authUrl = config.authUrl || window.location.origin + '/auth.html';
|
|
14
|
+
this.apiUrl = config.apiUrl || window.location.origin; // Base URL for TrulyYou API
|
|
15
|
+
this.targetElement = config.targetElement || null; // Element to render iframe in for visible mode
|
|
16
|
+
this.mockMobile = config.mockMobile || false; // Force mobile UI (for testing desktop-to-mobile handoff)
|
|
17
|
+
this.pendingRequests = new Map();
|
|
18
|
+
|
|
19
|
+
if (!this.appId) {
|
|
20
|
+
throw new Error('TrulyYou SDK: appId is required');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Listen for auth responses
|
|
24
|
+
window.addEventListener('message', this._handleAuthResponse.bind(this));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Fetch with TrulyYou signature
|
|
29
|
+
* @param {string} url - URL to fetch
|
|
30
|
+
* @param {Object} options - Fetch options
|
|
31
|
+
* @returns {Promise<Response>} - Fetch response with signature
|
|
32
|
+
*/
|
|
33
|
+
async fetchWithSignature(url, options = {}) {
|
|
34
|
+
const requestId = this._generateRequestId();
|
|
35
|
+
const method = options.method || 'GET';
|
|
36
|
+
const body = options.body || null;
|
|
37
|
+
|
|
38
|
+
// Parse URL to get path
|
|
39
|
+
let path;
|
|
40
|
+
try {
|
|
41
|
+
const urlObj = new URL(url, window.location.origin);
|
|
42
|
+
path = urlObj.pathname;
|
|
43
|
+
} catch (e) {
|
|
44
|
+
path = url;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Create session - this evaluates rules and returns everything we need
|
|
48
|
+
// Also discovers/creates the path (origin → endpoint → backend)
|
|
49
|
+
let sessionData = null;
|
|
50
|
+
try {
|
|
51
|
+
// Determine the actual backend URL
|
|
52
|
+
// - If URL is absolute (starts with http), extract the host from it
|
|
53
|
+
// - If URL is relative, the backend is this.apiUrl (where the request will actually go)
|
|
54
|
+
let backendUrl;
|
|
55
|
+
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
56
|
+
// Absolute URL - extract the backend from the URL itself
|
|
57
|
+
try {
|
|
58
|
+
const targetUrl = new URL(url);
|
|
59
|
+
backendUrl = `${targetUrl.protocol}//${targetUrl.host}`;
|
|
60
|
+
} catch (e) {
|
|
61
|
+
backendUrl = this.apiUrl || window.location.origin;
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
// Relative URL - the backend is apiUrl (where we'll prepend the path)
|
|
65
|
+
backendUrl = this.apiUrl || window.location.origin;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const sessionRes = await fetch(`${this.apiUrl || window.location.origin}/api/sessions`, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: { 'Content-Type': 'application/json' },
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
appId: this.appId,
|
|
73
|
+
requestId,
|
|
74
|
+
url: path,
|
|
75
|
+
method,
|
|
76
|
+
requestBody: body,
|
|
77
|
+
// Origin info for path discovery
|
|
78
|
+
originType: 'web',
|
|
79
|
+
webOrigin: window.location.origin,
|
|
80
|
+
backendUrl
|
|
81
|
+
})
|
|
82
|
+
});
|
|
83
|
+
if (sessionRes.ok) {
|
|
84
|
+
sessionData = await sessionRes.json();
|
|
85
|
+
|
|
86
|
+
// Check path status - if not active, show error to developer
|
|
87
|
+
if (sessionData.path) {
|
|
88
|
+
if (sessionData.path.status !== 'active') {
|
|
89
|
+
const pathDesc = `${sessionData.path.origin?.value} → ${sessionData.path.endpoint?.method} ${sessionData.path.endpoint?.path} → ${sessionData.path.backend?.baseUrl}`;
|
|
90
|
+
console.warn(`TrulyYou SDK: Path is not active (status: ${sessionData.path.status})`);
|
|
91
|
+
console.warn(`TrulyYou SDK: Path: ${pathDesc}`);
|
|
92
|
+
console.warn('TrulyYou SDK: To activate this path, go to your TrulyYou dashboard and set it to "active".');
|
|
93
|
+
|
|
94
|
+
throw new Error(`Path not active: ${pathDesc}. Status: ${sessionData.path.status}. Activate in TrulyYou dashboard.`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch (e) {
|
|
99
|
+
if (e.message && e.message.includes('Path not active')) {
|
|
100
|
+
throw e; // Re-throw path errors
|
|
101
|
+
}
|
|
102
|
+
console.warn('TrulyYou SDK: Could not create session, proceeding without requirements');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Create auth request with session data
|
|
106
|
+
const authRequest = {
|
|
107
|
+
url,
|
|
108
|
+
method,
|
|
109
|
+
body,
|
|
110
|
+
appId: this.appId,
|
|
111
|
+
requestId,
|
|
112
|
+
mode: this.mode,
|
|
113
|
+
// Session ID for auth page to fetch full context
|
|
114
|
+
sessionId: sessionData?.sessionId || null,
|
|
115
|
+
// Also pass evaluated data from session for immediate use
|
|
116
|
+
endpointTitle: sessionData?.endpointTitle || null,
|
|
117
|
+
endpointDescription: sessionData?.endpointDescription || null,
|
|
118
|
+
authRequirements: sessionData?.authRequirements || null
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Open auth window/iframe based on mode
|
|
122
|
+
const authResponse = await this._requestAuth(authRequest);
|
|
123
|
+
|
|
124
|
+
if (!authResponse.approved) {
|
|
125
|
+
throw new Error('Authentication denied by user');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Add signature headers from auth response
|
|
129
|
+
const headers = new Headers(options.headers || {});
|
|
130
|
+
|
|
131
|
+
// Auth page returns headers object with signature data
|
|
132
|
+
if (authResponse.headers) {
|
|
133
|
+
Object.entries(authResponse.headers).forEach(([key, value]) => {
|
|
134
|
+
headers.set(key, value);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Always add appId
|
|
139
|
+
headers.set('X-TrulyYou-AppId', this.appId);
|
|
140
|
+
|
|
141
|
+
// Make the actual request
|
|
142
|
+
// For relative URLs, prepend apiUrl to call the backend directly
|
|
143
|
+
const fetchUrl = (url.startsWith('http://') || url.startsWith('https://'))
|
|
144
|
+
? url
|
|
145
|
+
: `${this.apiUrl || window.location.origin}${url}`;
|
|
146
|
+
|
|
147
|
+
const response = await fetch(fetchUrl, {
|
|
148
|
+
...options,
|
|
149
|
+
headers
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return response;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Request authentication from user
|
|
157
|
+
* @private
|
|
158
|
+
*/
|
|
159
|
+
async _requestAuth(authRequest) {
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
// Store resolver for this request
|
|
162
|
+
this.pendingRequests.set(authRequest.requestId, { resolve, reject });
|
|
163
|
+
|
|
164
|
+
// Build auth URL - now simplified to just pass sessionId
|
|
165
|
+
// The auth page fetches all details from the session
|
|
166
|
+
const params = new URLSearchParams({
|
|
167
|
+
sessionId: authRequest.sessionId,
|
|
168
|
+
requestId: authRequest.requestId,
|
|
169
|
+
mode: this.mode
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (this.mockMobile) {
|
|
173
|
+
params.set('mockMobile', 'true');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const authUrl = `${this.authUrl}?${params.toString()}`;
|
|
177
|
+
|
|
178
|
+
if (this.mode === 'visible') {
|
|
179
|
+
if (this.targetElement) {
|
|
180
|
+
// Render iframe in target element
|
|
181
|
+
const targetEl = typeof this.targetElement === 'string'
|
|
182
|
+
? document.querySelector(this.targetElement)
|
|
183
|
+
: this.targetElement;
|
|
184
|
+
|
|
185
|
+
if (!targetEl) {
|
|
186
|
+
reject(new Error('Target element not found'));
|
|
187
|
+
this.pendingRequests.delete(authRequest.requestId);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Create iframe - auto-sizes to fit content
|
|
192
|
+
const iframe = document.createElement('iframe');
|
|
193
|
+
iframe.src = authUrl;
|
|
194
|
+
iframe.id = `truly-you-auth-${authRequest.requestId}`;
|
|
195
|
+
iframe.style.width = '100%';
|
|
196
|
+
iframe.style.height = '0';
|
|
197
|
+
iframe.style.border = 'none';
|
|
198
|
+
iframe.style.display = 'block';
|
|
199
|
+
iframe.style.overflow = 'hidden';
|
|
200
|
+
// Enable WebAuthn in cross-origin iframe
|
|
201
|
+
iframe.allow = 'publickey-credentials-get *; publickey-credentials-create *';
|
|
202
|
+
|
|
203
|
+
// Store original styles to restore later
|
|
204
|
+
this._originalContent = targetEl.innerHTML;
|
|
205
|
+
this._originalStyles = {
|
|
206
|
+
minHeight: targetEl.style.minHeight,
|
|
207
|
+
height: targetEl.style.height
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Listen for resize messages from iframe
|
|
211
|
+
const resizeHandler = (event) => {
|
|
212
|
+
if (event.data?.type === 'TRULY_YOU_RESIZE' && event.data?.requestId === authRequest.requestId) {
|
|
213
|
+
iframe.style.height = event.data.height + 'px';
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
window.addEventListener('message', resizeHandler);
|
|
217
|
+
this._resizeHandler = resizeHandler;
|
|
218
|
+
|
|
219
|
+
// Clear target and add iframe
|
|
220
|
+
targetEl.innerHTML = '';
|
|
221
|
+
targetEl.appendChild(iframe);
|
|
222
|
+
|
|
223
|
+
// Timeout after 5 minutes
|
|
224
|
+
setTimeout(() => {
|
|
225
|
+
if (this.pendingRequests.has(authRequest.requestId)) {
|
|
226
|
+
// Check localStorage for response
|
|
227
|
+
const storedResponse = localStorage.getItem(`truly_you_auth_${authRequest.requestId}`);
|
|
228
|
+
if (storedResponse) {
|
|
229
|
+
const response = JSON.parse(storedResponse);
|
|
230
|
+
localStorage.removeItem(`truly_you_auth_${authRequest.requestId}`);
|
|
231
|
+
|
|
232
|
+
if (response.approved) {
|
|
233
|
+
resolve(response);
|
|
234
|
+
} else {
|
|
235
|
+
reject(new Error('Authentication denied'));
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
reject(new Error('Authentication timeout'));
|
|
239
|
+
}
|
|
240
|
+
this.pendingRequests.delete(authRequest.requestId);
|
|
241
|
+
|
|
242
|
+
// Clean up resize handler
|
|
243
|
+
if (this._resizeHandler) {
|
|
244
|
+
window.removeEventListener('message', this._resizeHandler);
|
|
245
|
+
this._resizeHandler = undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Remove iframe and restore original content
|
|
249
|
+
if (iframe.parentNode) {
|
|
250
|
+
iframe.parentNode.removeChild(iframe);
|
|
251
|
+
if (this._originalContent !== undefined) {
|
|
252
|
+
targetEl.innerHTML = this._originalContent;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}, 300000);
|
|
257
|
+
|
|
258
|
+
} else {
|
|
259
|
+
// Open popup window (fallback when no targetElement provided)
|
|
260
|
+
const width = 480;
|
|
261
|
+
const height = 600;
|
|
262
|
+
const left = window.screen.width / 2 - width / 2;
|
|
263
|
+
const top = window.screen.height / 2 - height / 2;
|
|
264
|
+
|
|
265
|
+
const authWindow = window.open(
|
|
266
|
+
authUrl,
|
|
267
|
+
'TrulyYou Authentication',
|
|
268
|
+
`width=${width},height=${height},left=${left},top=${top},resizable=no,scrollbars=yes`
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
if (!authWindow) {
|
|
272
|
+
reject(new Error('Failed to open authentication window. Please allow popups.'));
|
|
273
|
+
this.pendingRequests.delete(authRequest.requestId);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Poll for window closure
|
|
278
|
+
const checkClosed = setInterval(() => {
|
|
279
|
+
if (authWindow.closed) {
|
|
280
|
+
clearInterval(checkClosed);
|
|
281
|
+
|
|
282
|
+
// Check localStorage for response
|
|
283
|
+
const storedResponse = localStorage.getItem(`truly_you_auth_${authRequest.requestId}`);
|
|
284
|
+
if (storedResponse) {
|
|
285
|
+
const response = JSON.parse(storedResponse);
|
|
286
|
+
localStorage.removeItem(`truly_you_auth_${authRequest.requestId}`);
|
|
287
|
+
|
|
288
|
+
if (response.approved) {
|
|
289
|
+
resolve(response);
|
|
290
|
+
} else {
|
|
291
|
+
reject(new Error('Authentication denied'));
|
|
292
|
+
}
|
|
293
|
+
this.pendingRequests.delete(authRequest.requestId);
|
|
294
|
+
} else {
|
|
295
|
+
reject(new Error('Authentication window closed without response'));
|
|
296
|
+
this.pendingRequests.delete(authRequest.requestId);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}, 500);
|
|
300
|
+
|
|
301
|
+
// Timeout after 5 minutes
|
|
302
|
+
setTimeout(() => {
|
|
303
|
+
if (this.pendingRequests.has(authRequest.requestId)) {
|
|
304
|
+
clearInterval(checkClosed);
|
|
305
|
+
if (authWindow && !authWindow.closed) {
|
|
306
|
+
authWindow.close();
|
|
307
|
+
}
|
|
308
|
+
reject(new Error('Authentication timeout'));
|
|
309
|
+
this.pendingRequests.delete(authRequest.requestId);
|
|
310
|
+
}
|
|
311
|
+
}, 300000);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
} else if (this.mode === 'hidden') {
|
|
315
|
+
// Check if we're on desktop - if so, we need to show QR code
|
|
316
|
+
const userAgent = navigator.userAgent.toLowerCase();
|
|
317
|
+
const isMac = /macintosh|mac os x/i.test(userAgent);
|
|
318
|
+
const isWindows = /windows nt/i.test(userAgent) && !/windows phone/i.test(userAgent);
|
|
319
|
+
const isLinuxDesktop = /linux/i.test(userAgent) && !/android/i.test(userAgent);
|
|
320
|
+
const isDesktop = isMac || isWindows || isLinuxDesktop;
|
|
321
|
+
|
|
322
|
+
console.log('[SDK] Hidden mode - isDesktop:', isDesktop, 'targetElement:', this.targetElement);
|
|
323
|
+
|
|
324
|
+
if (isDesktop && this.targetElement) {
|
|
325
|
+
// On desktop with targetElement, show visible iframe for QR code
|
|
326
|
+
// Find the visible element if selector matches multiple
|
|
327
|
+
let targetEl;
|
|
328
|
+
if (typeof this.targetElement === 'string') {
|
|
329
|
+
const elements = document.querySelectorAll(this.targetElement);
|
|
330
|
+
console.log('[SDK] Found elements:', elements.length);
|
|
331
|
+
targetEl = Array.from(elements).find(el => {
|
|
332
|
+
// Check if element AND all ancestors are visible
|
|
333
|
+
const isVisible = el.offsetParent !== null || el.offsetWidth > 0 || el.offsetHeight > 0;
|
|
334
|
+
console.log('[SDK] Element visible:', isVisible, 'offsetParent:', el.offsetParent, el);
|
|
335
|
+
return isVisible;
|
|
336
|
+
}) || elements[0];
|
|
337
|
+
} else {
|
|
338
|
+
targetEl = this.targetElement;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
console.log('[SDK] Using targetEl:', targetEl);
|
|
342
|
+
|
|
343
|
+
if (targetEl) {
|
|
344
|
+
const iframe = document.createElement('iframe');
|
|
345
|
+
iframe.src = authUrl;
|
|
346
|
+
iframe.id = `truly-you-auth-${authRequest.requestId}`;
|
|
347
|
+
iframe.style.width = '100%';
|
|
348
|
+
iframe.style.height = '0';
|
|
349
|
+
iframe.style.border = 'none';
|
|
350
|
+
iframe.style.display = 'block';
|
|
351
|
+
iframe.style.overflow = 'hidden';
|
|
352
|
+
iframe.allow = 'publickey-credentials-get *; publickey-credentials-create *';
|
|
353
|
+
|
|
354
|
+
// Store original styles to restore later
|
|
355
|
+
this._originalContent = targetEl.innerHTML;
|
|
356
|
+
this._originalStyles = {
|
|
357
|
+
minHeight: targetEl.style.minHeight,
|
|
358
|
+
height: targetEl.style.height
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// Listen for resize messages from iframe
|
|
362
|
+
const resizeHandler = (event) => {
|
|
363
|
+
if (event.data?.type === 'TRULY_YOU_RESIZE' && event.data?.requestId === authRequest.requestId) {
|
|
364
|
+
iframe.style.height = event.data.height + 'px';
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
window.addEventListener('message', resizeHandler);
|
|
368
|
+
this._resizeHandler = resizeHandler;
|
|
369
|
+
|
|
370
|
+
// Clear target and add iframe
|
|
371
|
+
targetEl.innerHTML = '';
|
|
372
|
+
targetEl.appendChild(iframe);
|
|
373
|
+
|
|
374
|
+
// Timeout after 10 minutes
|
|
375
|
+
setTimeout(() => {
|
|
376
|
+
if (this.pendingRequests.has(authRequest.requestId)) {
|
|
377
|
+
const storedResponse = localStorage.getItem(`truly_you_auth_${authRequest.requestId}`);
|
|
378
|
+
if (storedResponse) {
|
|
379
|
+
const response = JSON.parse(storedResponse);
|
|
380
|
+
localStorage.removeItem(`truly_you_auth_${authRequest.requestId}`);
|
|
381
|
+
|
|
382
|
+
if (response.approved) {
|
|
383
|
+
resolve(response);
|
|
384
|
+
} else {
|
|
385
|
+
reject(new Error('Authentication denied'));
|
|
386
|
+
}
|
|
387
|
+
} else {
|
|
388
|
+
reject(new Error('Authentication timeout'));
|
|
389
|
+
}
|
|
390
|
+
this.pendingRequests.delete(authRequest.requestId);
|
|
391
|
+
|
|
392
|
+
if (this._resizeHandler) {
|
|
393
|
+
window.removeEventListener('message', this._resizeHandler);
|
|
394
|
+
this._resizeHandler = undefined;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (iframe.parentNode) {
|
|
398
|
+
iframe.parentNode.removeChild(iframe);
|
|
399
|
+
if (this._originalContent !== undefined) {
|
|
400
|
+
targetEl.innerHTML = this._originalContent;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}, 600000);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Mobile or no targetElement: Create hidden iframe
|
|
410
|
+
const iframe = document.createElement('iframe');
|
|
411
|
+
iframe.style.display = 'none';
|
|
412
|
+
iframe.src = authUrl;
|
|
413
|
+
iframe.id = `truly-you-auth-${authRequest.requestId}`;
|
|
414
|
+
// Enable WebAuthn in cross-origin iframe
|
|
415
|
+
iframe.allow = 'publickey-credentials-get *; publickey-credentials-create *';
|
|
416
|
+
document.body.appendChild(iframe);
|
|
417
|
+
|
|
418
|
+
// Timeout after 10 minutes for hidden mode (enrollment can take time)
|
|
419
|
+
setTimeout(() => {
|
|
420
|
+
if (this.pendingRequests.has(authRequest.requestId)) {
|
|
421
|
+
document.body.removeChild(iframe);
|
|
422
|
+
|
|
423
|
+
// Check localStorage for response
|
|
424
|
+
const storedResponse = localStorage.getItem(`truly_you_auth_${authRequest.requestId}`);
|
|
425
|
+
if (storedResponse) {
|
|
426
|
+
const response = JSON.parse(storedResponse);
|
|
427
|
+
localStorage.removeItem(`truly_you_auth_${authRequest.requestId}`);
|
|
428
|
+
|
|
429
|
+
if (response.approved) {
|
|
430
|
+
resolve(response);
|
|
431
|
+
} else {
|
|
432
|
+
reject(new Error('Authentication denied'));
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
reject(new Error('Authentication timeout in hidden mode'));
|
|
436
|
+
}
|
|
437
|
+
this.pendingRequests.delete(authRequest.requestId);
|
|
438
|
+
}
|
|
439
|
+
}, 600000);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Handle auth response from window/iframe
|
|
446
|
+
* @private
|
|
447
|
+
*/
|
|
448
|
+
_handleAuthResponse(event) {
|
|
449
|
+
if (event.data?.type === 'TRULY_YOU_AUTH_RESPONSE') {
|
|
450
|
+
const response = event.data.data;
|
|
451
|
+
const pending = this.pendingRequests.get(response.requestId);
|
|
452
|
+
|
|
453
|
+
if (pending) {
|
|
454
|
+
if (response.approved) {
|
|
455
|
+
pending.resolve(response);
|
|
456
|
+
} else {
|
|
457
|
+
pending.reject(new Error('Authentication denied'));
|
|
458
|
+
}
|
|
459
|
+
this.pendingRequests.delete(response.requestId);
|
|
460
|
+
|
|
461
|
+
// Clean up resize handler
|
|
462
|
+
if (this._resizeHandler) {
|
|
463
|
+
window.removeEventListener('message', this._resizeHandler);
|
|
464
|
+
this._resizeHandler = undefined;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Clean up iframe if exists and restore original content
|
|
468
|
+
const iframe = document.getElementById(`truly-you-auth-${response.requestId}`);
|
|
469
|
+
if (iframe && iframe.parentNode) {
|
|
470
|
+
const parent = iframe.parentNode;
|
|
471
|
+
parent.removeChild(iframe);
|
|
472
|
+
|
|
473
|
+
// Restore original content if we saved it
|
|
474
|
+
if (this._originalContent !== undefined) {
|
|
475
|
+
parent.innerHTML = this._originalContent;
|
|
476
|
+
this._originalContent = undefined;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Generate unique request ID
|
|
485
|
+
* @private
|
|
486
|
+
*/
|
|
487
|
+
_generateRequestId() {
|
|
488
|
+
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Get current configuration
|
|
493
|
+
*/
|
|
494
|
+
getConfig() {
|
|
495
|
+
return {
|
|
496
|
+
appId: this.appId,
|
|
497
|
+
mode: this.mode,
|
|
498
|
+
authUrl: this.authUrl,
|
|
499
|
+
mockMobile: this.mockMobile
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Update mockMobile setting
|
|
505
|
+
*/
|
|
506
|
+
setMockMobile(enabled) {
|
|
507
|
+
this.mockMobile = !!enabled;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Update mode (visible/hidden)
|
|
512
|
+
*/
|
|
513
|
+
setMode(mode) {
|
|
514
|
+
if (mode !== 'visible' && mode !== 'hidden') {
|
|
515
|
+
throw new Error('Mode must be either "visible" or "hidden"');
|
|
516
|
+
}
|
|
517
|
+
this.mode = mode;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Export to window
|
|
522
|
+
window.TrulyYouSDK = TrulyYouSDK;
|
|
523
|
+
|
|
524
|
+
// AMD/CommonJS compatibility
|
|
525
|
+
if (typeof define === 'function' && define.amd) {
|
|
526
|
+
define([], function() { return TrulyYouSDK; });
|
|
527
|
+
} else if (typeof module === 'object' && module.exports) {
|
|
528
|
+
module.exports = TrulyYouSDK;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
})(typeof window !== 'undefined' ? window : global);
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* TrulyYou Server SDK - For server-side signature verification
|
|
535
|
+
* Usage: const TrulyYouServer = require('@trulyyou/sdk/server');
|
|
536
|
+
* const trulyyou = new TrulyYouServer({ appId: 'your-app-id', secretKey: 'your-secret-key' });
|
|
537
|
+
*/
|
|
538
|
+
(function(root) {
|
|
539
|
+
'use strict';
|
|
540
|
+
|
|
541
|
+
class TrulyYouServer {
|
|
542
|
+
/**
|
|
543
|
+
* Initialize the server-side SDK
|
|
544
|
+
* @param {object} config - Configuration options
|
|
545
|
+
* @param {string} config.appId - Your TrulyYou App ID (public key)
|
|
546
|
+
* @param {string} config.secretKey - Your TrulyYou Secret Key (private key)
|
|
547
|
+
* @param {string} [config.apiUrl] - TrulyYou API URL (defaults to https://api.trulyyou.io)
|
|
548
|
+
*/
|
|
549
|
+
constructor(config = {}) {
|
|
550
|
+
if (!config.appId) {
|
|
551
|
+
throw new Error('TrulyYou Server SDK: appId is required');
|
|
552
|
+
}
|
|
553
|
+
if (!config.secretKey) {
|
|
554
|
+
throw new Error('TrulyYou Server SDK: secretKey is required');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
this.appId = config.appId;
|
|
558
|
+
this.secretKey = config.secretKey;
|
|
559
|
+
this.apiUrl = config.apiUrl || process.env.TRULYYOU_API_URL || 'http://localhost:3000';
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Verify a signature from an incoming request
|
|
564
|
+
* @param {object} request - The incoming request object
|
|
565
|
+
* @param {object} request.headers - Request headers containing the signature
|
|
566
|
+
* @param {string} request.method - HTTP method (GET, POST, etc.)
|
|
567
|
+
* @param {string} request.url - Request URL/path
|
|
568
|
+
* @param {string|object} [request.body] - Request body (string or object)
|
|
569
|
+
* @returns {Promise<object>} - Verification result
|
|
570
|
+
*/
|
|
571
|
+
async verifySignature(request) {
|
|
572
|
+
const signature = request.headers['x-trulyyou-signature'] || request.headers['X-TrulyYou-Signature'];
|
|
573
|
+
const keyId = request.headers['x-trulyyou-keyid'] || request.headers['X-TrulyYou-KeyId'];
|
|
574
|
+
const payload = request.headers['x-trulyyou-payload'] || request.headers['X-TrulyYou-Payload'];
|
|
575
|
+
const appId = request.headers['x-trulyyou-appid'] || request.headers['X-TrulyYou-AppId'];
|
|
576
|
+
|
|
577
|
+
if (!signature) {
|
|
578
|
+
return {
|
|
579
|
+
verified: false,
|
|
580
|
+
error: 'Missing x-trulyyou-signature header'
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (appId && appId !== this.appId) {
|
|
585
|
+
return {
|
|
586
|
+
verified: false,
|
|
587
|
+
error: 'App ID mismatch'
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Prepare body
|
|
592
|
+
let body = request.body;
|
|
593
|
+
if (body && typeof body === 'object') {
|
|
594
|
+
body = JSON.stringify(body);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Call TrulyYou API to verify
|
|
598
|
+
try {
|
|
599
|
+
const fetch = typeof globalThis.fetch !== 'undefined'
|
|
600
|
+
? globalThis.fetch
|
|
601
|
+
: require('node-fetch');
|
|
602
|
+
|
|
603
|
+
const response = await fetch(`${this.apiUrl}/api/verify-signature`, {
|
|
604
|
+
method: 'POST',
|
|
605
|
+
headers: {
|
|
606
|
+
'Content-Type': 'application/json',
|
|
607
|
+
'X-TrulyYou-AppId': this.appId,
|
|
608
|
+
'X-TrulyYou-SecretKey': this.secretKey
|
|
609
|
+
},
|
|
610
|
+
body: JSON.stringify({
|
|
611
|
+
signature,
|
|
612
|
+
keyId: keyId || null,
|
|
613
|
+
payload: payload || null,
|
|
614
|
+
method: request.method,
|
|
615
|
+
url: request.url,
|
|
616
|
+
body: body || null
|
|
617
|
+
})
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
const result = await response.json();
|
|
621
|
+
|
|
622
|
+
return {
|
|
623
|
+
verified: result.verified || false,
|
|
624
|
+
user: result.user || null,
|
|
625
|
+
verifications: result.verifications || {},
|
|
626
|
+
authRequirementsMet: result.authRequirementsMet || false,
|
|
627
|
+
missingVerifications: result.missingVerifications || [],
|
|
628
|
+
error: result.error || null
|
|
629
|
+
};
|
|
630
|
+
} catch (err) {
|
|
631
|
+
return {
|
|
632
|
+
verified: false,
|
|
633
|
+
error: err.message
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Express/Connect middleware for signature verification
|
|
640
|
+
* @param {object} [options] - Middleware options
|
|
641
|
+
* @param {boolean} [options.optional=false] - If true, allows requests without signature
|
|
642
|
+
* @param {function} [options.onError] - Custom error handler
|
|
643
|
+
* @returns {function} Express middleware
|
|
644
|
+
*/
|
|
645
|
+
middleware(options = {}) {
|
|
646
|
+
const self = this;
|
|
647
|
+
|
|
648
|
+
return async function trulyYouMiddleware(req, res, next) {
|
|
649
|
+
const signature = req.headers['x-trulyyou-signature'];
|
|
650
|
+
|
|
651
|
+
// If signature is optional and not present, continue
|
|
652
|
+
if (options.optional && !signature) {
|
|
653
|
+
req.trulyYou = { verified: false, optional: true };
|
|
654
|
+
return next();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const result = await self.verifySignature({
|
|
658
|
+
headers: req.headers,
|
|
659
|
+
method: req.method,
|
|
660
|
+
url: req.originalUrl || req.url,
|
|
661
|
+
body: req.body
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
if (!result.verified) {
|
|
665
|
+
if (options.onError) {
|
|
666
|
+
return options.onError(result.error, req, res, next);
|
|
667
|
+
}
|
|
668
|
+
return res.status(401).json({
|
|
669
|
+
error: 'Unauthorized',
|
|
670
|
+
message: result.error || 'Invalid signature'
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Attach verification result to request
|
|
675
|
+
req.trulyYou = result;
|
|
676
|
+
next();
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Export for Node.js/CommonJS
|
|
682
|
+
if (typeof module === 'object' && module.exports) {
|
|
683
|
+
module.exports.TrulyYouServer = TrulyYouServer;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Export to global for browser (if needed)
|
|
687
|
+
if (typeof root !== 'undefined') {
|
|
688
|
+
root.TrulyYouServer = TrulyYouServer;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
})(typeof window !== 'undefined' ? window : (typeof global !== 'undefined' ? global : this));
|