@trustchex/react-native-sdk 1.490.1 → 1.492.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+
3
+ import { sha256 } from '@noble/hashes/sha2';
4
+ import { Buffer } from 'buffer';
5
+
6
+ /**
7
+ * Proof-of-work solver for the demo-diagnostics authenticity flow.
8
+ *
9
+ * No secret ships in the SDK. To upload a demo report, the SDK first mints a
10
+ * server-signed challenge token (`POST …/diagnostics/challenge`) and then finds
11
+ * a `solution` nonce such that `SHA256(challenge.solution)` has at least
12
+ * `difficulty` leading zero bits (hashcash-style). The server re-verifies the
13
+ * token signature (server-only key) and the PoW. See web-app diagnostic-pow.ts.
14
+ */
15
+
16
+ const leadingZeroBits = bytes => {
17
+ let bits = 0;
18
+ for (let i = 0; i < bytes.length; i++) {
19
+ const b = bytes[i];
20
+ if (b === 0) {
21
+ bits += 8;
22
+ continue;
23
+ }
24
+ // bits before the highest set bit in this byte (b is 1..255)
25
+ bits += Math.clz32(b) - 24;
26
+ break;
27
+ }
28
+ return bits;
29
+ };
30
+
31
+ /**
32
+ * Solve the PoW for a challenge. Returns the solution nonce as a string.
33
+ * Bounded by `maxIterations` so a pathological difficulty can't hang the app;
34
+ * returns null if unsolved within the bound.
35
+ */
36
+ export const solvePow = (challenge, difficulty, maxIterations = 8_000_000) => {
37
+ for (let i = 0; i < maxIterations; i++) {
38
+ const solution = i.toString();
39
+ const digest = sha256(Buffer.from(`${challenge}.${solution}`, 'utf-8'));
40
+ if (leadingZeroBits(digest) >= difficulty) {
41
+ return solution;
42
+ }
43
+ }
44
+ return null;
45
+ };
@@ -5,10 +5,12 @@
5
5
  *
6
6
  * This runs from the SDK's DEMO flow only, which is simulated client-side and
7
7
  * has no real verification session or account — so there's no session key to
8
- * encrypt with and nothing to authenticate against. The report is POSTed
9
- * unauthenticated to the global demo-diagnostics endpoint over HTTPS (encrypted
10
- * in transit); the backend encrypts the PII fields at rest. Internal developers
11
- * view and download the reports from a hidden dashboard page.
8
+ * encrypt with and no credential to authenticate with. Instead, authenticity is
9
+ * proven with a proof-of-work: the SDK mints a server-signed challenge token
10
+ * (no secret ships in the SDK), solves it, and submits the solution with the
11
+ * report over HTTPS. The backend verifies the token (server-only key) + PoW and
12
+ * encrypts the PII fields at rest. Internal developers view and download the
13
+ * reports from a hidden dashboard page.
12
14
  *
13
15
  * Uploaded payload (one JSON body):
14
16
  * - non-PII technical metadata (platform, sdkVersion, documentType,
@@ -17,11 +19,10 @@
17
19
  * - `scanDataJson` — the scanned identity data (PII),
18
20
  * - `images` — the captured document/face images (base64, PII).
19
21
  */
20
- import 'react-native-get-random-values';
21
22
  import { diagnostics } from "./diagnostics.js";
22
23
  import { gatherDeviceDetails } from "./native-device-info.utils.js";
23
24
  import { buildDiagnosticReport } from "./diagnosticReport.js";
24
- import { signDiagnosticBody } from "./diagnosticAuth.js";
25
+ import { solvePow } from "./diagnosticPow.js";
25
26
  import { logError } from "./debug.utils.js";
26
27
  const mimeToType = mime => mime === 'png' ? 'image/png' : 'image/jpeg';
27
28
 
@@ -80,23 +81,34 @@ export const sendDiagnosticReport = async params => {
80
81
  }))
81
82
  };
82
83
 
83
- // Sign the EXACT body bytes so the backend can verify authenticity. Use
84
- // fetch directly (httpClient doesn't expose custom headers / raw body).
85
- const rawBody = JSON.stringify(payload);
86
- const {
87
- timestamp,
88
- nonce,
89
- signature
90
- } = signDiagnosticBody(rawBody);
84
+ // Authenticity via proof of work: mint a server-signed challenge, solve it,
85
+ // and submit the solution. No secret ships in the SDK.
86
+ const challengeRes = await fetch(`${baseUrl}/api/app/mobile/diagnostics/challenge`, {
87
+ method: 'POST',
88
+ headers: {
89
+ 'Content-Type': 'application/json'
90
+ }
91
+ });
92
+ if (!challengeRes.ok) {
93
+ return {
94
+ uploaded: false
95
+ };
96
+ }
97
+ const challenge = await challengeRes.json();
98
+ const solution = solvePow(challenge.challenge, challenge.difficulty);
99
+ if (!solution) {
100
+ return {
101
+ uploaded: false
102
+ };
103
+ }
91
104
  const response = await fetch(`${baseUrl}/api/app/mobile/diagnostics`, {
92
105
  method: 'POST',
93
106
  headers: {
94
107
  'Content-Type': 'application/json',
95
- 'X-Trustchex-Timestamp': timestamp,
96
- 'X-Trustchex-Nonce': nonce,
97
- 'X-Trustchex-Signature': signature
108
+ 'X-Trustchex-Pow-Token': challenge.token,
109
+ 'X-Trustchex-Pow-Solution': solution
98
110
  },
99
- body: rawBody
111
+ body: JSON.stringify(payload)
100
112
  });
101
113
  return {
102
114
  uploaded: response.ok
@@ -2,4 +2,4 @@
2
2
 
3
3
  // This file is auto-generated. Do not edit manually.
4
4
  // Version is synced from package.json during build.
5
- export const SDK_VERSION = '1.490.1';
5
+ export const SDK_VERSION = '1.492.0';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Proof-of-work solver for the demo-diagnostics authenticity flow.
3
+ *
4
+ * No secret ships in the SDK. To upload a demo report, the SDK first mints a
5
+ * server-signed challenge token (`POST …/diagnostics/challenge`) and then finds
6
+ * a `solution` nonce such that `SHA256(challenge.solution)` has at least
7
+ * `difficulty` leading zero bits (hashcash-style). The server re-verifies the
8
+ * token signature (server-only key) and the PoW. See web-app diagnostic-pow.ts.
9
+ */
10
+ export interface PowChallenge {
11
+ token: string;
12
+ challenge: string;
13
+ difficulty: number;
14
+ expiresAt: number;
15
+ }
16
+ /**
17
+ * Solve the PoW for a challenge. Returns the solution nonce as a string.
18
+ * Bounded by `maxIterations` so a pathological difficulty can't hang the app;
19
+ * returns null if unsolved within the bound.
20
+ */
21
+ export declare const solvePow: (challenge: string, difficulty: number, maxIterations?: number) => string | null;
22
+ //# sourceMappingURL=diagnosticPow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnosticPow.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/diagnosticPow.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AAEH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAiBD;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GACnB,WAAW,MAAM,EACjB,YAAY,MAAM,EAClB,sBAAyB,KACxB,MAAM,GAAG,IASX,CAAC"}
@@ -1,21 +1,3 @@
1
- /**
2
- * Assembles and uploads the support diagnostic report to the backend.
3
- *
4
- * This runs from the SDK's DEMO flow only, which is simulated client-side and
5
- * has no real verification session or account — so there's no session key to
6
- * encrypt with and nothing to authenticate against. The report is POSTed
7
- * unauthenticated to the global demo-diagnostics endpoint over HTTPS (encrypted
8
- * in transit); the backend encrypts the PII fields at rest. Internal developers
9
- * view and download the reports from a hidden dashboard page.
10
- *
11
- * Uploaded payload (one JSON body):
12
- * - non-PII technical metadata (platform, sdkVersion, documentType,
13
- * nfcErrorCode, paceFallbackReason, succeeded) for the list/filter,
14
- * - `diagnosticsJson` — technical signals (device, camera, MRZ, NFC steps),
15
- * - `scanDataJson` — the scanned identity data (PII),
16
- * - `images` — the captured document/face images (base64, PII).
17
- */
18
- import 'react-native-get-random-values';
19
1
  import { type ScanDataForReport } from './diagnosticReport';
20
2
  export interface DiagnosticImage {
21
3
  /** Stable file name without extension, e.g. "mrz-side". */
@@ -1 +1 @@
1
- {"version":3,"file":"sendDiagnosticReport.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/sendDiagnosticReport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,gCAAgC,CAAC;AAGxC,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,oBAAoB,CAAC;AAI5B,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,iBAAiB,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,UAAU;IACzB,oDAAoD;IACpD,QAAQ,EAAE,OAAO,CAAC;CACnB;AAKD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,oBAAoB,KAC3B,OAAO,CAAC,UAAU,CA6EpB,CAAC"}
1
+ {"version":3,"file":"sendDiagnosticReport.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/sendDiagnosticReport.ts"],"names":[],"mappings":"AAqBA,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,oBAAoB,CAAC;AAI5B,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,iBAAiB,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,UAAU;IACzB,oDAAoD;IACpD,QAAQ,EAAE,OAAO,CAAC;CACnB;AAKD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,oBAAoB,KAC3B,OAAO,CAAC,UAAU,CAsFpB,CAAC"}
@@ -1,2 +1,2 @@
1
- export declare const SDK_VERSION = "1.490.1";
1
+ export declare const SDK_VERSION = "1.492.0";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trustchex/react-native-sdk",
3
- "version": "1.490.1",
3
+ "version": "1.492.0",
4
4
  "description": "Trustchex mobile app react native SDK for android or ios devices",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -0,0 +1,54 @@
1
+ import { sha256 } from '@noble/hashes/sha2';
2
+ import { Buffer } from 'buffer';
3
+
4
+ /**
5
+ * Proof-of-work solver for the demo-diagnostics authenticity flow.
6
+ *
7
+ * No secret ships in the SDK. To upload a demo report, the SDK first mints a
8
+ * server-signed challenge token (`POST …/diagnostics/challenge`) and then finds
9
+ * a `solution` nonce such that `SHA256(challenge.solution)` has at least
10
+ * `difficulty` leading zero bits (hashcash-style). The server re-verifies the
11
+ * token signature (server-only key) and the PoW. See web-app diagnostic-pow.ts.
12
+ */
13
+
14
+ export interface PowChallenge {
15
+ token: string;
16
+ challenge: string;
17
+ difficulty: number;
18
+ expiresAt: number;
19
+ }
20
+
21
+ const leadingZeroBits = (bytes: Uint8Array): number => {
22
+ let bits = 0;
23
+ for (let i = 0; i < bytes.length; i++) {
24
+ const b = bytes[i];
25
+ if (b === 0) {
26
+ bits += 8;
27
+ continue;
28
+ }
29
+ // bits before the highest set bit in this byte (b is 1..255)
30
+ bits += Math.clz32(b) - 24;
31
+ break;
32
+ }
33
+ return bits;
34
+ };
35
+
36
+ /**
37
+ * Solve the PoW for a challenge. Returns the solution nonce as a string.
38
+ * Bounded by `maxIterations` so a pathological difficulty can't hang the app;
39
+ * returns null if unsolved within the bound.
40
+ */
41
+ export const solvePow = (
42
+ challenge: string,
43
+ difficulty: number,
44
+ maxIterations = 8_000_000
45
+ ): string | null => {
46
+ for (let i = 0; i < maxIterations; i++) {
47
+ const solution = i.toString();
48
+ const digest = sha256(Buffer.from(`${challenge}.${solution}`, 'utf-8'));
49
+ if (leadingZeroBits(digest) >= difficulty) {
50
+ return solution;
51
+ }
52
+ }
53
+ return null;
54
+ };
@@ -3,10 +3,12 @@
3
3
  *
4
4
  * This runs from the SDK's DEMO flow only, which is simulated client-side and
5
5
  * has no real verification session or account — so there's no session key to
6
- * encrypt with and nothing to authenticate against. The report is POSTed
7
- * unauthenticated to the global demo-diagnostics endpoint over HTTPS (encrypted
8
- * in transit); the backend encrypts the PII fields at rest. Internal developers
9
- * view and download the reports from a hidden dashboard page.
6
+ * encrypt with and no credential to authenticate with. Instead, authenticity is
7
+ * proven with a proof-of-work: the SDK mints a server-signed challenge token
8
+ * (no secret ships in the SDK), solves it, and submits the solution with the
9
+ * report over HTTPS. The backend verifies the token (server-only key) + PoW and
10
+ * encrypts the PII fields at rest. Internal developers view and download the
11
+ * reports from a hidden dashboard page.
10
12
  *
11
13
  * Uploaded payload (one JSON body):
12
14
  * - non-PII technical metadata (platform, sdkVersion, documentType,
@@ -15,14 +17,13 @@
15
17
  * - `scanDataJson` — the scanned identity data (PII),
16
18
  * - `images` — the captured document/face images (base64, PII).
17
19
  */
18
- import 'react-native-get-random-values';
19
20
  import { diagnostics } from './diagnostics';
20
21
  import { gatherDeviceDetails } from './native-device-info.utils';
21
22
  import {
22
23
  buildDiagnosticReport,
23
24
  type ScanDataForReport,
24
25
  } from './diagnosticReport';
25
- import { signDiagnosticBody } from './diagnosticAuth';
26
+ import { solvePow, type PowChallenge } from './diagnosticPow';
26
27
  import { logError } from './debug.utils';
27
28
 
28
29
  export interface DiagnosticImage {
@@ -110,20 +111,29 @@ export const sendDiagnosticReport = async (
110
111
  })),
111
112
  };
112
113
 
113
- // Sign the EXACT body bytes so the backend can verify authenticity. Use
114
- // fetch directly (httpClient doesn't expose custom headers / raw body).
115
- const rawBody = JSON.stringify(payload);
116
- const { timestamp, nonce, signature } = signDiagnosticBody(rawBody);
114
+ // Authenticity via proof of work: mint a server-signed challenge, solve it,
115
+ // and submit the solution. No secret ships in the SDK.
116
+ const challengeRes = await fetch(
117
+ `${baseUrl}/api/app/mobile/diagnostics/challenge`,
118
+ { method: 'POST', headers: { 'Content-Type': 'application/json' } }
119
+ );
120
+ if (!challengeRes.ok) {
121
+ return { uploaded: false };
122
+ }
123
+ const challenge = (await challengeRes.json()) as PowChallenge;
124
+ const solution = solvePow(challenge.challenge, challenge.difficulty);
125
+ if (!solution) {
126
+ return { uploaded: false };
127
+ }
117
128
 
118
129
  const response = await fetch(`${baseUrl}/api/app/mobile/diagnostics`, {
119
130
  method: 'POST',
120
131
  headers: {
121
132
  'Content-Type': 'application/json',
122
- 'X-Trustchex-Timestamp': timestamp,
123
- 'X-Trustchex-Nonce': nonce,
124
- 'X-Trustchex-Signature': signature,
133
+ 'X-Trustchex-Pow-Token': challenge.token,
134
+ 'X-Trustchex-Pow-Solution': solution,
125
135
  },
126
- body: rawBody,
136
+ body: JSON.stringify(payload),
127
137
  });
128
138
 
129
139
  return { uploaded: response.ok };
package/src/version.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // This file is auto-generated. Do not edit manually.
2
2
  // Version is synced from package.json during build.
3
- export const SDK_VERSION = '1.490.1';
3
+ export const SDK_VERSION = '1.492.0';
@@ -1,36 +0,0 @@
1
- "use strict";
2
-
3
- import { Buffer } from 'buffer';
4
- import { hmac } from '@noble/hashes/hmac';
5
- import { sha256 } from '@noble/hashes/sha2';
6
-
7
- /**
8
- * Authenticity signing for the demo-diagnostics upload.
9
- *
10
- * Demo reports have no session/account to authenticate against, so the SDK signs
11
- * each upload with HMAC-SHA256 over `timestamp.nonce.rawBody` using a shared
12
- * secret. The backend recomputes and rejects mismatches and stale timestamps
13
- * (replay). The secret ships in the SDK, so it's an obfuscation barrier (paired
14
- * with server rate limiting), not a true secret — rotate via the env var on both
15
- * sides. MUST stay byte-for-byte in sync with web-app/src/lib/diagnostic-hmac.ts.
16
- */
17
- const SECRET = process.env.TRUSTCHEX_DIAGNOSTICS_SECRET ?? 'tcx-diag-v1-2f9c4a7e8b1d4f63a05e9c7b3d61f8a2';
18
- const randomNonce = () => {
19
- // 16 random bytes as hex. crypto.getRandomValues is polyfilled by
20
- // react-native-get-random-values (already imported in crypto.utils).
21
- const bytes = new Uint8Array(16);
22
- globalThis.crypto.getRandomValues(bytes);
23
- return Buffer.from(bytes).toString('hex');
24
- };
25
-
26
- /** Sign a raw JSON body for the demo-diagnostics endpoint. */
27
- export const signDiagnosticBody = rawBody => {
28
- const timestamp = Date.now().toString();
29
- const nonce = randomNonce();
30
- const mac = hmac(sha256, Buffer.from(SECRET, 'utf-8'), Buffer.from(`${timestamp}.${nonce}.${rawBody}`, 'utf-8'));
31
- return {
32
- timestamp,
33
- nonce,
34
- signature: Buffer.from(mac).toString('hex')
35
- };
36
- };
@@ -1,8 +0,0 @@
1
- export interface DiagnosticSignature {
2
- timestamp: string;
3
- nonce: string;
4
- signature: string;
5
- }
6
- /** Sign a raw JSON body for the demo-diagnostics endpoint. */
7
- export declare const signDiagnosticBody: (rawBody: string) => DiagnosticSignature;
8
- //# sourceMappingURL=diagnosticAuth.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"diagnosticAuth.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/diagnosticAuth.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAUD,8DAA8D;AAC9D,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG,mBAapD,CAAC"}
@@ -1,47 +0,0 @@
1
- import { Buffer } from 'buffer';
2
- import { hmac } from '@noble/hashes/hmac';
3
- import { sha256 } from '@noble/hashes/sha2';
4
-
5
- /**
6
- * Authenticity signing for the demo-diagnostics upload.
7
- *
8
- * Demo reports have no session/account to authenticate against, so the SDK signs
9
- * each upload with HMAC-SHA256 over `timestamp.nonce.rawBody` using a shared
10
- * secret. The backend recomputes and rejects mismatches and stale timestamps
11
- * (replay). The secret ships in the SDK, so it's an obfuscation barrier (paired
12
- * with server rate limiting), not a true secret — rotate via the env var on both
13
- * sides. MUST stay byte-for-byte in sync with web-app/src/lib/diagnostic-hmac.ts.
14
- */
15
- const SECRET =
16
- process.env.TRUSTCHEX_DIAGNOSTICS_SECRET ??
17
- 'tcx-diag-v1-2f9c4a7e8b1d4f63a05e9c7b3d61f8a2';
18
-
19
- export interface DiagnosticSignature {
20
- timestamp: string;
21
- nonce: string;
22
- signature: string;
23
- }
24
-
25
- const randomNonce = (): string => {
26
- // 16 random bytes as hex. crypto.getRandomValues is polyfilled by
27
- // react-native-get-random-values (already imported in crypto.utils).
28
- const bytes = new Uint8Array(16);
29
- globalThis.crypto.getRandomValues(bytes);
30
- return Buffer.from(bytes).toString('hex');
31
- };
32
-
33
- /** Sign a raw JSON body for the demo-diagnostics endpoint. */
34
- export const signDiagnosticBody = (rawBody: string): DiagnosticSignature => {
35
- const timestamp = Date.now().toString();
36
- const nonce = randomNonce();
37
- const mac = hmac(
38
- sha256,
39
- Buffer.from(SECRET, 'utf-8'),
40
- Buffer.from(`${timestamp}.${nonce}.${rawBody}`, 'utf-8')
41
- );
42
- return {
43
- timestamp,
44
- nonce,
45
- signature: Buffer.from(mac).toString('hex'),
46
- };
47
- };