@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.
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +19 -21
- package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -0
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +3 -2
- package/lib/module/Screens/Static/QrCodeScanningScreen.js +12 -2
- package/lib/module/Screens/Static/ResultScreen.js +103 -107
- package/lib/module/Shared/Components/DebugOverlay.js +5 -2
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +53 -22
- package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +8 -1
- package/lib/module/Shared/EIDReader/eidReader.js +15 -14
- package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +34 -0
- package/lib/module/Shared/Libs/SignalingClient.js +6 -5
- package/lib/module/Shared/Libs/crypto.utils.js +25 -3
- package/lib/module/Shared/Libs/deeplink.utils.js +37 -7
- package/lib/module/Shared/Libs/http-client.js +19 -6
- package/lib/module/Shared/Libs/mrz.utils.js +193 -33
- package/lib/module/Shared/Libs/mrzFrameAggregator.js +45 -1
- package/lib/module/Shared/Libs/promise.utils.js +4 -3
- package/lib/module/Shared/Libs/shuffle.utils.js +17 -0
- package/lib/module/Shared/Services/AnalyticsService.js +8 -2
- package/lib/module/Shared/Services/DataUploadService.js +114 -127
- package/lib/module/Shared/Services/VideoSessionService.js +4 -3
- package/lib/module/Translation/Resources/en.js +1 -0
- package/lib/module/Translation/Resources/tr.js +1 -0
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DebugOverlay.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/crypto.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts +1 -0
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/http-client.d.ts +2 -0
- package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +22 -0
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts +3 -0
- package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +1 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +1 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +13 -3
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +4 -3
- package/src/Screens/Static/QrCodeScanningScreen.tsx +19 -2
- package/src/Screens/Static/ResultScreen.tsx +126 -161
- package/src/Shared/Components/DebugOverlay.tsx +5 -2
- package/src/Shared/Components/IdentityDocumentCamera.tsx +55 -22
- package/src/Shared/Components/IdentityDocumentCamera.utils.ts +9 -1
- package/src/Shared/EIDReader/eidReader.ts +23 -14
- package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +34 -0
- package/src/Shared/Libs/SignalingClient.ts +10 -5
- package/src/Shared/Libs/crypto.utils.ts +41 -4
- package/src/Shared/Libs/deeplink.utils.ts +39 -7
- package/src/Shared/Libs/http-client.ts +24 -8
- package/src/Shared/Libs/mrz.utils.ts +200 -34
- package/src/Shared/Libs/mrzFrameAggregator.ts +53 -1
- package/src/Shared/Libs/promise.utils.ts +11 -5
- package/src/Shared/Libs/shuffle.utils.ts +15 -0
- package/src/Shared/Services/AnalyticsService.ts +17 -1
- package/src/Shared/Services/DataUploadService.ts +155 -166
- package/src/Shared/Services/VideoSessionService.ts +15 -3
- package/src/Translation/Resources/en.ts +1 -0
- package/src/Translation/Resources/tr.ts +1 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
291
|
+
debugLog('EID', '[EID] SELECT MF OK');
|
|
291
292
|
} catch (mfErr) {
|
|
292
293
|
const mfMsg = mfErr instanceof Error ? mfErr.message : String(mfErr);
|
|
293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
338
|
+
debugLog('EID', '[EID] PACE succeeded');
|
|
338
339
|
} else {
|
|
339
|
-
|
|
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
|
-
|
|
345
|
+
debugLog('EID', `[EID] PACE protocol failed (${msg}), falling back to BAC`);
|
|
345
346
|
if (paceError instanceof Error && paceError.stack) {
|
|
346
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
68
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
9
|
-
const route = url.
|
|
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
|
-
|
|
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
|
-
|
|
60
|
+
debugLog('HTTP', `${httpMethod} ${url}`);
|
|
51
61
|
if (body) {
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
+
debugLog('HTTP', 'Response body:', JSON.stringify(responseJson).substring(0, 200) + '...');
|
|
70
83
|
}
|
|
71
84
|
} catch (error) {
|
|
72
85
|
// Invalid JSON response
|
|
73
|
-
|
|
86
|
+
debugLog('HTTP', 'Response body: (non-JSON)');
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
// Track API call performance (non-blocking)
|