@trustchex/react-native-sdk 1.475.1 → 1.478.7

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 (75) hide show
  1. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +19 -21
  2. package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -0
  3. package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +3 -2
  4. package/lib/module/Screens/Static/QrCodeScanningScreen.js +12 -2
  5. package/lib/module/Screens/Static/ResultScreen.js +103 -107
  6. package/lib/module/Shared/Components/DebugOverlay.js +5 -2
  7. package/lib/module/Shared/Components/IdentityDocumentCamera.js +53 -22
  8. package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +8 -1
  9. package/lib/module/Shared/EIDReader/eidReader.js +15 -14
  10. package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +34 -0
  11. package/lib/module/Shared/Libs/SignalingClient.js +6 -5
  12. package/lib/module/Shared/Libs/crypto.utils.js +25 -3
  13. package/lib/module/Shared/Libs/deeplink.utils.js +37 -7
  14. package/lib/module/Shared/Libs/http-client.js +19 -6
  15. package/lib/module/Shared/Libs/mrz.utils.js +193 -33
  16. package/lib/module/Shared/Libs/mrzFrameAggregator.js +45 -1
  17. package/lib/module/Shared/Libs/promise.utils.js +4 -3
  18. package/lib/module/Shared/Libs/shuffle.utils.js +17 -0
  19. package/lib/module/Shared/Services/AnalyticsService.js +8 -2
  20. package/lib/module/Shared/Services/DataUploadService.js +114 -127
  21. package/lib/module/Shared/Services/VideoSessionService.js +4 -3
  22. package/lib/module/Translation/Resources/en.js +1 -0
  23. package/lib/module/Translation/Resources/tr.js +1 -0
  24. package/lib/module/version.js +1 -1
  25. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  26. package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
  27. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  28. package/lib/typescript/src/Shared/Components/DebugOverlay.d.ts.map +1 -1
  29. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  30. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +1 -1
  31. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
  32. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
  33. package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts.map +1 -1
  34. package/lib/typescript/src/Shared/Libs/crypto.utils.d.ts.map +1 -1
  35. package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts +1 -0
  36. package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
  37. package/lib/typescript/src/Shared/Libs/http-client.d.ts +2 -0
  38. package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
  39. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  40. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +22 -0
  41. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -1
  42. package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
  43. package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts +3 -0
  44. package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts.map +1 -0
  45. package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -1
  46. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
  47. package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts.map +1 -1
  48. package/lib/typescript/src/Translation/Resources/en.d.ts +1 -0
  49. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  50. package/lib/typescript/src/Translation/Resources/tr.d.ts +1 -0
  51. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  52. package/lib/typescript/src/version.d.ts +1 -1
  53. package/package.json +13 -3
  54. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +4 -3
  55. package/src/Screens/Static/QrCodeScanningScreen.tsx +19 -2
  56. package/src/Screens/Static/ResultScreen.tsx +126 -161
  57. package/src/Shared/Components/DebugOverlay.tsx +5 -2
  58. package/src/Shared/Components/IdentityDocumentCamera.tsx +55 -22
  59. package/src/Shared/Components/IdentityDocumentCamera.utils.ts +9 -1
  60. package/src/Shared/EIDReader/eidReader.ts +23 -14
  61. package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +34 -0
  62. package/src/Shared/Libs/SignalingClient.ts +10 -5
  63. package/src/Shared/Libs/crypto.utils.ts +41 -4
  64. package/src/Shared/Libs/deeplink.utils.ts +39 -7
  65. package/src/Shared/Libs/http-client.ts +24 -8
  66. package/src/Shared/Libs/mrz.utils.ts +200 -34
  67. package/src/Shared/Libs/mrzFrameAggregator.ts +53 -1
  68. package/src/Shared/Libs/promise.utils.ts +11 -5
  69. package/src/Shared/Libs/shuffle.utils.ts +15 -0
  70. package/src/Shared/Services/AnalyticsService.ts +17 -1
  71. package/src/Shared/Services/DataUploadService.ts +155 -166
  72. package/src/Shared/Services/VideoSessionService.ts +15 -3
  73. package/src/Translation/Resources/en.ts +1 -0
  74. package/src/Translation/Resources/tr.ts +1 -0
  75. package/src/version.ts +1 -1
@@ -8,6 +8,7 @@ import { DG1File } from "./lds/icao/dg1File.js";
8
8
  import { DG2File } from "./lds/icao/dg2File.js";
9
9
  import { Buffer } from 'buffer';
10
10
  import { InputStream } from "./java/inputStream.js";
11
+ import { debugLog } from "../Libs/debug.utils.js";
11
12
 
12
13
  // --- Minimal PNG encoder (pure JS, no native deps) ---
13
14
  // Uses deflate stored (uncompressed) blocks for O(n) encoding speed.
@@ -147,7 +148,7 @@ function convertJP2IfNeeded(imageBuffer, mimeType) {
147
148
  mimeType
148
149
  };
149
150
  }
150
- console.debug('[EID] Face image is JP2, attempting conversion…');
151
+ debugLog('EID', '[EID] Face image is JP2, attempting conversion…');
151
152
  try {
152
153
  // Lazy-load jpeg2000 to avoid crashing if it fails to import
153
154
  const {
@@ -156,16 +157,16 @@ function convertJP2IfNeeded(imageBuffer, mimeType) {
156
157
  const jpx = new JpxImage();
157
158
  jpx.parse(imageBuffer);
158
159
  const tile = jpx.tiles[0];
159
- console.debug(`[EID] JP2 decoded: ${tile.width}x${tile.height}, ${jpx.componentsCount} channels, ${tile.items.length} bytes`);
160
+ debugLog('EID', `[EID] JP2 decoded: ${tile.width}x${tile.height}, ${jpx.componentsCount} channels, ${tile.items.length} bytes`);
160
161
  const pngBytes = pixelsToPng(tile.items, tile.width, tile.height, jpx.componentsCount);
161
162
  const pngBase64 = Buffer.from(pngBytes).toString('base64');
162
- console.debug(`[EID] Converted JP2 → PNG (${pngBytes.length} bytes, ${pngBase64.length} base64 chars)`);
163
+ debugLog('EID', `[EID] Converted JP2 → PNG (${pngBytes.length} bytes, ${pngBase64.length} base64 chars)`);
163
164
  return {
164
165
  base64: pngBase64,
165
166
  mimeType: 'image/png'
166
167
  };
167
168
  } catch (err) {
168
- console.debug('[EID] JP2 conversion failed:', err);
169
+ debugLog('EID', '[EID] JP2 conversion failed:', err);
169
170
  return {
170
171
  base64: imageBuffer.toString('base64'),
171
172
  mimeType
@@ -287,10 +288,10 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
287
288
  let hasPACESucceeded = false;
288
289
  try {
289
290
  await passportService.sendSelectMF();
290
- console.debug('[EID] SELECT MF OK');
291
+ debugLog('EID', '[EID] SELECT MF OK');
291
292
  } catch (mfErr) {
292
293
  const mfMsg = mfErr instanceof Error ? mfErr.message : String(mfErr);
293
- console.debug(`[EID] SELECT MF failed (${mfMsg}), continuing with SFI read anyway`);
294
+ debugLog('EID', `[EID] SELECT MF failed (${mfMsg}), continuing with SFI read anyway`);
294
295
  }
295
296
  progress = 2;
296
297
  if (progressCallback) {
@@ -309,10 +310,10 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
309
310
  buf[i] = await cardAccessStream.read();
310
311
  }
311
312
  cardAccessBuf = buf;
312
- console.debug(`[EID] EF.CardAccess read OK: ${cardAccessLen} bytes = ${cardAccessBuf.toString('hex')}`);
313
+ debugLog('EID', `[EID] EF.CardAccess read OK: ${cardAccessLen} bytes = ${cardAccessBuf.toString('hex')}`);
313
314
  } catch (readErr) {
314
315
  const readMsg = readErr instanceof Error ? readErr.message : String(readErr);
315
- console.debug(`[EID] EF.CardAccess read FAILED: ${readMsg} — falling back to BAC`);
316
+ debugLog('EID', `[EID] EF.CardAccess read FAILED: ${readMsg} — falling back to BAC`);
316
317
  }
317
318
  progress = 3;
318
319
  if (progressCallback) {
@@ -324,7 +325,7 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
324
325
  try {
325
326
  const paceInfo = parsePACEInfoFromCardAccess(cardAccessBuf);
326
327
  if (paceInfo) {
327
- console.debug(`[EID] PACE available: oid=${paceInfo.oid} parameterId=${paceInfo.parameterId}`);
328
+ debugLog('EID', `[EID] PACE available: oid=${paceInfo.oid} parameterId=${paceInfo.parameterId}`);
328
329
  // PACE is available - derive key from MRZ and perform PACE
329
330
  const bacKey = new BACKey(documentNumber, dateOfBirth, dateOfExpiry);
330
331
  const paceKey = PACEKeySpec.createMRZKey(bacKey.getKeySeedForPACE());
@@ -334,16 +335,16 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
334
335
  }
335
336
  await passportService.doPACE(paceKey, paceInfo.oid, paceInfo.parameterId, progressCallback);
336
337
  hasPACESucceeded = true;
337
- console.debug('[EID] PACE succeeded');
338
+ debugLog('EID', '[EID] PACE succeeded');
338
339
  } else {
339
- console.debug('[EID] EF.CardAccess read OK but no PACE SecurityInfo found — document does not support PACE, falling back to BAC');
340
+ debugLog('EID', '[EID] EF.CardAccess read OK but no PACE SecurityInfo found — document does not support PACE, falling back to BAC');
340
341
  }
341
342
  } catch (paceError) {
342
343
  // PACE protocol exchange failed — fall back to BAC
343
344
  const msg = paceError instanceof Error ? paceError.message : String(paceError);
344
- console.debug(`[EID] PACE protocol failed (${msg}), falling back to BAC`);
345
+ debugLog('EID', `[EID] PACE protocol failed (${msg}), falling back to BAC`);
345
346
  if (paceError instanceof Error && paceError.stack) {
346
- console.debug('[EID] PACE error stack:', paceError.stack);
347
+ debugLog('EID', '[EID] PACE error stack:', paceError.stack);
347
348
  }
348
349
  }
349
350
  }
@@ -423,7 +424,7 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
423
424
  mimeType: mimeType
424
425
  };
425
426
  } catch (error) {
426
- console.debug('Error reading passport data', error);
427
+ debugLog('EID', 'Error reading passport data', error);
427
428
  } finally {
428
429
  await passportService.close();
429
430
  }
@@ -56,6 +56,40 @@ primary safeguard. A single noisy frame is not trusted; the consensus is.
56
56
 
57
57
  ---
58
58
 
59
+ ## 2a. Turkish e-ID document numbers: leading serial LETTER must be preserved
60
+
61
+ **Issue.** Turkish identity cards put a **serial letter** at the start of the
62
+ 9-char document number (e.g. `A53032352`, `A49M38410`). Empirically — verified
63
+ against real cards — **Türkiye computes the document-number check digit over the
64
+ number with the LEADING letter EXCLUDED** (interior letters are kept), which
65
+ diverges from strict ICAO 9303 (where `A`=10 is weighted in). So a genuine card
66
+ `A53032352` (printed check digit `5`) **fails** the strict ICAO check (which
67
+ wants `7`), even though the card is valid — `icaoCheckDigit("53032352") === "5"`.
68
+ The TD1 **composite** on these cards is likewise non-standard (no interpretation
69
+ of the leading letter reproduces it), so it is **not** a reliable anchor.
70
+
71
+ **Why it matters.** The leading letter is **real** and **must be preserved**: the
72
+ **NFC eID chip derives its BAC/PACE access key from the document number exactly
73
+ as stored** (with the letter). Rewriting `A53032352`→`453032352` — as the
74
+ ambiguous-char recovery would, to satisfy strict ICAO — makes the **chip read
75
+ FAIL**. (The OCR-B `4` glyph closely resembles `A`, which is why the recovery
76
+ used to "correct" it.)
77
+
78
+ **Mitigation.** `acceptsTurkishLetterPrefix` (in `mrz.utils.ts`; ported to
79
+ `mrz_utils.dart`) accepts a TUR TD1 whose only failing checks are
80
+ document-number/composite **when** the document number starts with a letter
81
+ **and** `icaoCheckDigit(documentNumber.slice(1))` matches the printed
82
+ document-number check digit. The parse is then accepted **as-is**, preserving the
83
+ letter; the composite is deliberately not required. The subsequent **NFC read is
84
+ the authoritative confirmation** — it only succeeds with the correct
85
+ (letter-preserving) number. Contrast `A49M38410`, whose letters validate under
86
+ *standard* ICAO and so are kept by the normal path.
87
+
88
+ Covered by the "Turkish e-ID … leading serial LETTER" tests in
89
+ `mrz.ambiguous.test.ts` (RN) and `mrz_ambiguous_test.dart` (Flutter).
90
+
91
+ ---
92
+
59
93
  ## 3. Filler-letter normalisation is conservative in name fields (by design)
60
94
 
61
95
  **Issue.** The `<` filler is frequently OCR'd as a letter (`K`, and under glare
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  import EventSource from 'react-native-sse';
4
+ import { debugLog } from "./debug.utils.js";
4
5
  export class SignalingClient {
5
6
  eventSource = null;
6
7
  constructor(baseUrl, sessionId, onMessage, identificationId, onSessionEnded) {
@@ -19,13 +20,13 @@ export class SignalingClient {
19
20
  urlParams.append('identificationId', this.identificationId);
20
21
  }
21
22
  const url = `${this.baseUrl}/api/app/mobile/video-sessions/${this.sessionId}/signaling/stream?${urlParams.toString()}`;
22
- console.log('[SignalingClient] Connecting to SSE:', url);
23
+ debugLog('SignalingClient', '[SignalingClient] Connecting to SSE:', url);
23
24
  this.eventSource = new EventSource(url, {
24
25
  pollingInterval: 0 // 0 means default/no polling if SSE is real
25
26
  });
26
27
  const listener = event => {
27
28
  if (event.type === 'open') {
28
- console.log('[SignalingClient] Connected');
29
+ debugLog('SignalingClient', '[SignalingClient] Connected');
29
30
  this.onConnected?.();
30
31
  } else if (event.type === 'error') {
31
32
  console.error('[SignalingClient] Connection error:', JSON.stringify(event, null, 2));
@@ -64,8 +65,8 @@ export class SignalingClient {
64
65
  };
65
66
 
66
67
  // Ping handler - server sends pings to keep connection alive
67
- const pingListener = event => {
68
- console.log('[SignalingClient] Received ping');
68
+ const pingListener = _event => {
69
+ debugLog('SignalingClient', '[SignalingClient] Received ping');
69
70
  // Just acknowledge by doing nothing, server-side heartbeat keeps connection alive
70
71
  };
71
72
 
@@ -73,7 +74,7 @@ export class SignalingClient {
73
74
  const sessionEndedListener = event => {
74
75
  try {
75
76
  const data = JSON.parse(event.data || '{}');
76
- console.log('[SignalingClient] Session ended:', data.state);
77
+ debugLog('SignalingClient', '[SignalingClient] Session ended:', data.state);
77
78
  if (this.onSessionEnded) {
78
79
  this.onSessionEnded(data.state);
79
80
  }
@@ -5,19 +5,41 @@ import { Buffer } from 'buffer';
5
5
  import { gcm } from '@noble/ciphers/aes';
6
6
  import { randomBytes } from '@noble/ciphers/webcrypto';
7
7
  import { secp256k1 } from '@noble/curves/secp256k1';
8
- import httpClient from "./http-client.js";
8
+ import { hkdf } from '@noble/hashes/hkdf';
9
+ import { sha256 } from '@noble/hashes/sha2';
10
+ import httpClient, { setSessionToken } from "./http-client.js";
11
+
12
+ // Must stay byte-for-byte identical to the backend derivation
13
+ // (web-app/src/lib/crypto-utils.ts).
14
+ const SESSION_KEY_KDF_INFO = 'trustchex-session-key-v2';
15
+ const SESSION_KEY_KDF_VERSION = 2;
9
16
  const getSessionKey = async (apiUrl, sessionId) => {
10
17
  const clientPrivateKey = secp256k1.utils.randomPrivateKey();
11
18
  const clientPublicKey = secp256k1.getPublicKey(clientPrivateKey);
12
19
  const serverResponse = await httpClient.post(`${apiUrl}/verification-sessions/${sessionId}/auth`, {
13
- clientPublicKey: Buffer.from(clientPublicKey).toString('hex')
20
+ clientPublicKey: Buffer.from(clientPublicKey).toString('hex'),
21
+ kdfVersion: SESSION_KEY_KDF_VERSION
14
22
  });
23
+ if (serverResponse.sessionToken) {
24
+ setSessionToken(serverResponse.sessionToken);
25
+ }
15
26
  const sharedKey = secp256k1.getSharedSecret(clientPrivateKey, Buffer.from(serverResponse.serverPublicKey, 'hex').toString('hex'));
27
+
28
+ // The server echoes the kdfVersion it actually used; older backends don't
29
+ // send the field and derive the legacy key.
30
+ if (serverResponse.kdfVersion === SESSION_KEY_KDF_VERSION) {
31
+ // sharedKey is a compressed secp256k1 point; byte 0 is the 0x02/0x03
32
+ // parity prefix and carries no secret entropy.
33
+ const derived = hkdf(sha256, Buffer.from(sharedKey).subarray(1), Buffer.from(sessionId, 'utf-8'), SESSION_KEY_KDF_INFO, 32);
34
+ return Buffer.from(derived).toString('hex');
35
+ }
16
36
  const sessionKey = (Buffer.from(new Date().getUTCDate().toString(), 'utf-8').toString('hex') + Buffer.from(sharedKey).toString('hex')).substring(0, 32);
17
37
  return sessionKey;
18
38
  };
19
39
  const encryptWithAes = (data, key) => {
20
- const nonce = randomBytes(32);
40
+ // Standard 96-bit GCM IV; the backend reads the nonce from the payload, so
41
+ // this stays compatible with servers that previously saw 32-byte nonces.
42
+ const nonce = randomBytes(12);
21
43
  const dataBytes = Buffer.from(data, 'utf-8');
22
44
  const aes = gcm(Buffer.from(key, 'hex'), nonce);
23
45
  const ciphertext = aes.encrypt(dataBytes);
@@ -1,12 +1,30 @@
1
1
  "use strict";
2
2
 
3
- import { debugLog } from "./debug.utils.js";
3
+ import { debugLog, logWarn } from "./debug.utils.js";
4
+
5
+ // Hostname (optionally with port) — rejects paths, credentials, and other
6
+ // URL components smuggled into the app-url segment.
7
+ const HOST_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9.-]*(:\d{1,5})?$/;
8
+ const getUrlScheme = url => {
9
+ const match = url.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\//);
10
+ return match ? match[1].toLowerCase() : '';
11
+ };
12
+
13
+ // The SDK uploads identity documents and biometrics to the derived baseUrl, so
14
+ // cleartext destinations are only allowed in development builds.
15
+ const isAllowedScheme = scheme => {
16
+ return scheme === 'https' || __DEV__ && scheme === 'http';
17
+ };
18
+ export const getUrlHost = url => {
19
+ const withoutScheme = url.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, '');
20
+ return (withoutScheme.split('/')[0] ?? '').toLowerCase();
21
+ };
4
22
  const handleDeepLink = ({
5
23
  url
6
24
  }) => {
7
25
  debugLog('handleDeepLink', 'Received URL:', url);
8
- const isHttps = url.includes('https');
9
- const route = url.replace(/http(s)?(:)?(\/\/)?/g, '');
26
+ const scheme = getUrlScheme(url);
27
+ const route = scheme ? url.slice(`${scheme}://`.length) : url;
10
28
  const segments = route.split('/');
11
29
  debugLog('handleDeepLink', 'Parsed segments:', segments);
12
30
  let baseUrl = '';
@@ -16,16 +34,28 @@ const handleDeepLink = ({
16
34
  sessionId = segments[i + 1] ?? '';
17
35
  debugLog('handleDeepLink', 'Found sessionId:', sessionId);
18
36
  } else if (segments[i] === 'app-url') {
19
- baseUrl = `${isHttps ? 'https' : 'http'}://${segments[i + 1]}`;
37
+ const host = segments[i + 1] ?? '';
38
+ if (!HOST_PATTERN.test(host)) {
39
+ logWarn('handleDeepLink', 'Rejected invalid app-url host');
40
+ continue;
41
+ }
42
+ const hostScheme = scheme === 'http' ? 'http' : 'https';
43
+ if (!isAllowedScheme(hostScheme)) {
44
+ logWarn('handleDeepLink', 'Rejected non-HTTPS app-url');
45
+ continue;
46
+ }
47
+ baseUrl = `${hostScheme}://${host}`;
20
48
  debugLog('handleDeepLink', 'Found baseUrl:', baseUrl);
21
49
  }
22
50
  }
23
51
 
24
52
  // If no app-url segment found, derive baseUrl from the URL itself
25
53
  if (!baseUrl && sessionId) {
26
- const match = url.match(/^(https?:\/\/[^/]+)/);
27
- if (match) {
28
- baseUrl = match[1];
54
+ const match = url.match(/^(https?):\/\/([^/]+)/);
55
+ if (match && isAllowedScheme(match[1].toLowerCase())) {
56
+ baseUrl = `${match[1].toLowerCase()}://${match[2]}`;
57
+ } else if (match) {
58
+ logWarn('handleDeepLink', 'Rejected non-HTTPS deep link URL');
29
59
  }
30
60
  }
31
61
  debugLog('handleDeepLink', 'Returning:', {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  import { trackApiCall, trackError } from "./analytics.utils.js";
4
+ import { debugLog } from "./debug.utils.js";
4
5
  export class HttpClientError extends Error {
5
6
  constructor(message) {
6
7
  super(message);
@@ -36,6 +37,15 @@ export class InternalServerError extends HttpClientError {
36
37
  super(message ?? 'Internal server error');
37
38
  }
38
39
  }
40
+
41
+ // Short-lived per-verification token issued by the backend during the session
42
+ // key exchange; sent on every subsequent call. Older backends don't issue one,
43
+ // in which case requests go out without the header (backwards compatible).
44
+ let sessionToken = null;
45
+ export const setSessionToken = token => {
46
+ sessionToken = token;
47
+ };
48
+ export const getSessionToken = () => sessionToken;
39
49
  const request = async (httpMethod, url, body, simulatedResponse) => {
40
50
  if (simulatedResponse) {
41
51
  return new Promise(resolve => {
@@ -47,30 +57,33 @@ const request = async (httpMethod, url, body, simulatedResponse) => {
47
57
  const startTime = Date.now();
48
58
  let statusCode = 0;
49
59
  let success = false;
50
- console.log(`[HTTP] ${httpMethod} ${url}`);
60
+ debugLog('HTTP', `${httpMethod} ${url}`);
51
61
  if (body) {
52
- console.log('[HTTP] Request body:', JSON.stringify(body).substring(0, 200) + '...');
62
+ debugLog('HTTP', 'Request body:', JSON.stringify(body).substring(0, 200) + '...');
53
63
  }
54
64
  try {
55
65
  const response = await fetch(url, {
56
66
  method: httpMethod,
57
67
  headers: {
58
- 'Content-Type': 'application/json'
68
+ 'Content-Type': 'application/json',
69
+ ...(sessionToken ? {
70
+ 'X-Session-Token': sessionToken
71
+ } : {})
59
72
  },
60
73
  body: body ? JSON.stringify(body) : undefined
61
74
  });
62
75
  statusCode = response.status;
63
76
  success = response.ok;
64
- console.log(`[HTTP] Response status: ${statusCode} ${response.ok ? '✓' : '✗'}`);
77
+ debugLog('HTTP', `Response status: ${statusCode} ${response.ok ? '✓' : '✗'}`);
65
78
  let responseJson = null;
66
79
  try {
67
80
  responseJson = await response.json();
68
81
  if (responseJson) {
69
- console.log('[HTTP] Response body:', JSON.stringify(responseJson).substring(0, 200) + '...');
82
+ debugLog('HTTP', 'Response body:', JSON.stringify(responseJson).substring(0, 200) + '...');
70
83
  }
71
84
  } catch (error) {
72
85
  // Invalid JSON response
73
- console.log('[HTTP] Response body: (non-JSON)');
86
+ debugLog('HTTP', 'Response body: (non-JSON)');
74
87
  }
75
88
 
76
89
  // Track API call performance (non-blocking)