@truly-you/trulyyou-web-sdk 0.1.23 → 0.1.25

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.js ADDED
@@ -0,0 +1,687 @@
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 appId = request.headers['x-trulyyou-appid'] || request.headers['X-TrulyYou-AppId'];
574
+
575
+ if (!signature) {
576
+ return {
577
+ verified: false,
578
+ error: 'Missing x-trulyyou-signature header'
579
+ };
580
+ }
581
+
582
+ if (appId && appId !== this.appId) {
583
+ return {
584
+ verified: false,
585
+ error: 'App ID mismatch'
586
+ };
587
+ }
588
+
589
+ // Prepare body
590
+ let body = request.body;
591
+ if (body && typeof body === 'object') {
592
+ body = JSON.stringify(body);
593
+ }
594
+
595
+ // Call TrulyYou API to verify
596
+ try {
597
+ const fetch = typeof globalThis.fetch !== 'undefined'
598
+ ? globalThis.fetch
599
+ : require('node-fetch');
600
+
601
+ const response = await fetch(`${this.apiUrl}/api/verify-signature`, {
602
+ method: 'POST',
603
+ headers: {
604
+ 'Content-Type': 'application/json',
605
+ 'X-TrulyYou-AppId': this.appId,
606
+ 'X-TrulyYou-SecretKey': this.secretKey
607
+ },
608
+ body: JSON.stringify({
609
+ signature,
610
+ method: request.method,
611
+ url: request.url,
612
+ body: body || null
613
+ })
614
+ });
615
+
616
+ const result = await response.json();
617
+
618
+ return {
619
+ verified: result.verified || false,
620
+ user: result.user || null,
621
+ verifications: result.verifications || {},
622
+ authRequirementsMet: result.authRequirementsMet || false,
623
+ missingVerifications: result.missingVerifications || [],
624
+ error: result.error || null
625
+ };
626
+ } catch (err) {
627
+ return {
628
+ verified: false,
629
+ error: err.message
630
+ };
631
+ }
632
+ }
633
+
634
+ /**
635
+ * Express/Connect middleware for signature verification
636
+ * @param {object} [options] - Middleware options
637
+ * @param {boolean} [options.optional=false] - If true, allows requests without signature
638
+ * @param {function} [options.onError] - Custom error handler
639
+ * @returns {function} Express middleware
640
+ */
641
+ middleware(options = {}) {
642
+ const self = this;
643
+
644
+ return async function trulyYouMiddleware(req, res, next) {
645
+ const signature = req.headers['x-trulyyou-signature'];
646
+
647
+ // If signature is optional and not present, continue
648
+ if (options.optional && !signature) {
649
+ req.trulyYou = { verified: false, optional: true };
650
+ return next();
651
+ }
652
+
653
+ const result = await self.verifySignature({
654
+ headers: req.headers,
655
+ method: req.method,
656
+ url: req.originalUrl || req.url,
657
+ body: req.body
658
+ });
659
+
660
+ if (!result.verified) {
661
+ if (options.onError) {
662
+ return options.onError(result.error, req, res, next);
663
+ }
664
+ return res.status(401).json({
665
+ error: 'Unauthorized',
666
+ message: result.error || 'Invalid signature'
667
+ });
668
+ }
669
+
670
+ // Attach verification result to request
671
+ req.trulyYou = result;
672
+ next();
673
+ };
674
+ }
675
+ }
676
+
677
+ // Export for Node.js/CommonJS
678
+ if (typeof module === 'object' && module.exports) {
679
+ module.exports.TrulyYouServer = TrulyYouServer;
680
+ }
681
+
682
+ // Export to global for browser (if needed)
683
+ if (typeof root !== 'undefined') {
684
+ root.TrulyYouServer = TrulyYouServer;
685
+ }
686
+
687
+ })(typeof window !== 'undefined' ? window : (typeof global !== 'undefined' ? global : this));