@kwik-id/sdk-react-native 0.0.0-stage → 0.1.0-alpha.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.
- package/LICENSE +22 -0
- package/README.md +81 -2
- package/app.plugin.js +40 -0
- package/lib/commonjs/api/api-client.js +213 -0
- package/lib/commonjs/api/api-client.js.map +1 -0
- package/lib/commonjs/api/api-error.js +29 -0
- package/lib/commonjs/api/api-error.js.map +1 -0
- package/lib/commonjs/api/index.js +33 -0
- package/lib/commonjs/api/index.js.map +1 -0
- package/lib/commonjs/api/upload.js +74 -0
- package/lib/commonjs/api/upload.js.map +1 -0
- package/lib/commonjs/camera/camera-view.js +227 -0
- package/lib/commonjs/camera/camera-view.js.map +1 -0
- package/lib/commonjs/camera/index.js +20 -0
- package/lib/commonjs/camera/index.js.map +1 -0
- package/lib/commonjs/camera/photo-capture.js +50 -0
- package/lib/commonjs/camera/photo-capture.js.map +1 -0
- package/lib/commonjs/crypto/fingerprint.js +43 -0
- package/lib/commonjs/crypto/fingerprint.js.map +1 -0
- package/lib/commonjs/crypto/hmac.js +150 -0
- package/lib/commonjs/crypto/hmac.js.map +1 -0
- package/lib/commonjs/crypto/index.js +39 -0
- package/lib/commonjs/crypto/index.js.map +1 -0
- package/lib/commonjs/crypto/session-manager.js +173 -0
- package/lib/commonjs/crypto/session-manager.js.map +1 -0
- package/lib/commonjs/i18n/en.js +297 -0
- package/lib/commonjs/i18n/en.js.map +1 -0
- package/lib/commonjs/i18n/fr.js +294 -0
- package/lib/commonjs/i18n/fr.js.map +1 -0
- package/lib/commonjs/i18n/index.js +34 -0
- package/lib/commonjs/i18n/index.js.map +1 -0
- package/lib/commonjs/i18n/locale.js +20 -0
- package/lib/commonjs/i18n/locale.js.map +1 -0
- package/lib/commonjs/i18n/translator.js +37 -0
- package/lib/commonjs/i18n/translator.js.map +1 -0
- package/lib/commonjs/index.js +187 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/lib/device-detector.js +34 -0
- package/lib/commonjs/lib/device-detector.js.map +1 -0
- package/lib/commonjs/lib/event-emitter.js +85 -0
- package/lib/commonjs/lib/event-emitter.js.map +1 -0
- package/lib/commonjs/lib/index.js +63 -0
- package/lib/commonjs/lib/index.js.map +1 -0
- package/lib/commonjs/lib/kwik-id-rn-sdk.js +156 -0
- package/lib/commonjs/lib/kwik-id-rn-sdk.js.map +1 -0
- package/lib/commonjs/lib/verification-machine.js +276 -0
- package/lib/commonjs/lib/verification-machine.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types/events.js +6 -0
- package/lib/commonjs/types/events.js.map +1 -0
- package/lib/commonjs/types/index.js +2 -0
- package/lib/commonjs/types/index.js.map +1 -0
- package/lib/commonjs/ui/components/KwikIDVerificationModal.js +407 -0
- package/lib/commonjs/ui/components/KwikIDVerificationModal.js.map +1 -0
- package/lib/commonjs/ui/components/screens.js +338 -0
- package/lib/commonjs/ui/components/screens.js.map +1 -0
- package/lib/commonjs/ui/context/kwik-id-provider.js +52 -0
- package/lib/commonjs/ui/context/kwik-id-provider.js.map +1 -0
- package/lib/commonjs/ui/hooks/use-kwik-verification.js +77 -0
- package/lib/commonjs/ui/hooks/use-kwik-verification.js.map +1 -0
- package/lib/commonjs/ui/index.js +33 -0
- package/lib/commonjs/ui/index.js.map +1 -0
- package/lib/module/api/api-client.js +208 -0
- package/lib/module/api/api-client.js.map +1 -0
- package/lib/module/api/api-error.js +24 -0
- package/lib/module/api/api-error.js.map +1 -0
- package/lib/module/api/index.js +6 -0
- package/lib/module/api/index.js.map +1 -0
- package/lib/module/api/upload.js +68 -0
- package/lib/module/api/upload.js.map +1 -0
- package/lib/module/camera/camera-view.js +222 -0
- package/lib/module/camera/camera-view.js.map +1 -0
- package/lib/module/camera/index.js +5 -0
- package/lib/module/camera/index.js.map +1 -0
- package/lib/module/camera/photo-capture.js +45 -0
- package/lib/module/camera/photo-capture.js.map +1 -0
- package/lib/module/crypto/fingerprint.js +37 -0
- package/lib/module/crypto/fingerprint.js.map +1 -0
- package/lib/module/crypto/hmac.js +143 -0
- package/lib/module/crypto/hmac.js.map +1 -0
- package/lib/module/crypto/index.js +6 -0
- package/lib/module/crypto/index.js.map +1 -0
- package/lib/module/crypto/session-manager.js +169 -0
- package/lib/module/crypto/session-manager.js.map +1 -0
- package/lib/module/i18n/en.js +293 -0
- package/lib/module/i18n/en.js.map +1 -0
- package/lib/module/i18n/fr.js +290 -0
- package/lib/module/i18n/fr.js.map +1 -0
- package/lib/module/i18n/index.js +11 -0
- package/lib/module/i18n/index.js.map +1 -0
- package/lib/module/i18n/locale.js +15 -0
- package/lib/module/i18n/locale.js.map +1 -0
- package/lib/module/i18n/translator.js +34 -0
- package/lib/module/i18n/translator.js.map +1 -0
- package/lib/module/index.js +30 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/lib/device-detector.js +26 -0
- package/lib/module/lib/device-detector.js.map +1 -0
- package/lib/module/lib/event-emitter.js +80 -0
- package/lib/module/lib/event-emitter.js.map +1 -0
- package/lib/module/lib/index.js +6 -0
- package/lib/module/lib/index.js.map +1 -0
- package/lib/module/lib/kwik-id-rn-sdk.js +150 -0
- package/lib/module/lib/kwik-id-rn-sdk.js.map +1 -0
- package/lib/module/lib/verification-machine.js +268 -0
- package/lib/module/lib/verification-machine.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types/events.js +4 -0
- package/lib/module/types/events.js.map +1 -0
- package/lib/module/types/index.js +2 -0
- package/lib/module/types/index.js.map +1 -0
- package/lib/module/ui/components/KwikIDVerificationModal.js +402 -0
- package/lib/module/ui/components/KwikIDVerificationModal.js.map +1 -0
- package/lib/module/ui/components/screens.js +328 -0
- package/lib/module/ui/components/screens.js.map +1 -0
- package/lib/module/ui/context/kwik-id-provider.js +46 -0
- package/lib/module/ui/context/kwik-id-provider.js.map +1 -0
- package/lib/module/ui/hooks/use-kwik-verification.js +72 -0
- package/lib/module/ui/hooks/use-kwik-verification.js.map +1 -0
- package/lib/module/ui/index.js +6 -0
- package/lib/module/ui/index.js.map +1 -0
- package/lib/typescript/commonjs/api/api-client.d.ts +82 -0
- package/lib/typescript/commonjs/api/api-client.d.ts.map +1 -0
- package/lib/typescript/commonjs/api/api-error.d.ts +13 -0
- package/lib/typescript/commonjs/api/api-error.d.ts.map +1 -0
- package/lib/typescript/commonjs/api/index.d.ts +6 -0
- package/lib/typescript/commonjs/api/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/api/upload.d.ts +19 -0
- package/lib/typescript/commonjs/api/upload.d.ts.map +1 -0
- package/lib/typescript/commonjs/camera/camera-view.d.ts +25 -0
- package/lib/typescript/commonjs/camera/camera-view.d.ts.map +1 -0
- package/lib/typescript/commonjs/camera/index.d.ts +5 -0
- package/lib/typescript/commonjs/camera/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/camera/photo-capture.d.ts +9 -0
- package/lib/typescript/commonjs/camera/photo-capture.d.ts.map +1 -0
- package/lib/typescript/commonjs/crypto/fingerprint.d.ts +3 -0
- package/lib/typescript/commonjs/crypto/fingerprint.d.ts.map +1 -0
- package/lib/typescript/commonjs/crypto/hmac.d.ts +18 -0
- package/lib/typescript/commonjs/crypto/hmac.d.ts.map +1 -0
- package/lib/typescript/commonjs/crypto/index.d.ts +4 -0
- package/lib/typescript/commonjs/crypto/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/crypto/session-manager.d.ts +27 -0
- package/lib/typescript/commonjs/crypto/session-manager.d.ts.map +1 -0
- package/lib/typescript/commonjs/i18n/en.d.ts +291 -0
- package/lib/typescript/commonjs/i18n/en.d.ts.map +1 -0
- package/lib/typescript/commonjs/i18n/fr.d.ts +6 -0
- package/lib/typescript/commonjs/i18n/fr.d.ts.map +1 -0
- package/lib/typescript/commonjs/i18n/index.d.ts +10 -0
- package/lib/typescript/commonjs/i18n/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/i18n/locale.d.ts +4 -0
- package/lib/typescript/commonjs/i18n/locale.d.ts.map +1 -0
- package/lib/typescript/commonjs/i18n/translator.d.ts +15 -0
- package/lib/typescript/commonjs/i18n/translator.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +24 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/lib/device-detector.d.ts +5 -0
- package/lib/typescript/commonjs/lib/device-detector.d.ts.map +1 -0
- package/lib/typescript/commonjs/lib/event-emitter.d.ts +19 -0
- package/lib/typescript/commonjs/lib/event-emitter.d.ts.map +1 -0
- package/lib/typescript/commonjs/lib/index.d.ts +5 -0
- package/lib/typescript/commonjs/lib/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/lib/kwik-id-rn-sdk.d.ts +43 -0
- package/lib/typescript/commonjs/lib/kwik-id-rn-sdk.d.ts.map +1 -0
- package/lib/typescript/commonjs/lib/verification-machine.d.ts +78 -0
- package/lib/typescript/commonjs/lib/verification-machine.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/types/events.d.ts +94 -0
- package/lib/typescript/commonjs/types/events.d.ts.map +1 -0
- package/lib/typescript/commonjs/types/index.d.ts +242 -0
- package/lib/typescript/commonjs/types/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/KwikIDVerificationModal.d.ts +21 -0
- package/lib/typescript/commonjs/ui/components/KwikIDVerificationModal.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/screens.d.ts +46 -0
- package/lib/typescript/commonjs/ui/components/screens.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/context/kwik-id-provider.d.ts +19 -0
- package/lib/typescript/commonjs/ui/context/kwik-id-provider.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/use-kwik-verification.d.ts +29 -0
- package/lib/typescript/commonjs/ui/hooks/use-kwik-verification.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/index.d.ts +7 -0
- package/lib/typescript/commonjs/ui/index.d.ts.map +1 -0
- package/lib/typescript/module/api/api-client.d.ts +82 -0
- package/lib/typescript/module/api/api-client.d.ts.map +1 -0
- package/lib/typescript/module/api/api-error.d.ts +13 -0
- package/lib/typescript/module/api/api-error.d.ts.map +1 -0
- package/lib/typescript/module/api/index.d.ts +6 -0
- package/lib/typescript/module/api/index.d.ts.map +1 -0
- package/lib/typescript/module/api/upload.d.ts +19 -0
- package/lib/typescript/module/api/upload.d.ts.map +1 -0
- package/lib/typescript/module/camera/camera-view.d.ts +25 -0
- package/lib/typescript/module/camera/camera-view.d.ts.map +1 -0
- package/lib/typescript/module/camera/index.d.ts +5 -0
- package/lib/typescript/module/camera/index.d.ts.map +1 -0
- package/lib/typescript/module/camera/photo-capture.d.ts +9 -0
- package/lib/typescript/module/camera/photo-capture.d.ts.map +1 -0
- package/lib/typescript/module/crypto/fingerprint.d.ts +3 -0
- package/lib/typescript/module/crypto/fingerprint.d.ts.map +1 -0
- package/lib/typescript/module/crypto/hmac.d.ts +18 -0
- package/lib/typescript/module/crypto/hmac.d.ts.map +1 -0
- package/lib/typescript/module/crypto/index.d.ts +4 -0
- package/lib/typescript/module/crypto/index.d.ts.map +1 -0
- package/lib/typescript/module/crypto/session-manager.d.ts +27 -0
- package/lib/typescript/module/crypto/session-manager.d.ts.map +1 -0
- package/lib/typescript/module/i18n/en.d.ts +291 -0
- package/lib/typescript/module/i18n/en.d.ts.map +1 -0
- package/lib/typescript/module/i18n/fr.d.ts +6 -0
- package/lib/typescript/module/i18n/fr.d.ts.map +1 -0
- package/lib/typescript/module/i18n/index.d.ts +10 -0
- package/lib/typescript/module/i18n/index.d.ts.map +1 -0
- package/lib/typescript/module/i18n/locale.d.ts +4 -0
- package/lib/typescript/module/i18n/locale.d.ts.map +1 -0
- package/lib/typescript/module/i18n/translator.d.ts +15 -0
- package/lib/typescript/module/i18n/translator.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +24 -0
- package/lib/typescript/module/index.d.ts.map +1 -0
- package/lib/typescript/module/lib/device-detector.d.ts +5 -0
- package/lib/typescript/module/lib/device-detector.d.ts.map +1 -0
- package/lib/typescript/module/lib/event-emitter.d.ts +19 -0
- package/lib/typescript/module/lib/event-emitter.d.ts.map +1 -0
- package/lib/typescript/module/lib/index.d.ts +5 -0
- package/lib/typescript/module/lib/index.d.ts.map +1 -0
- package/lib/typescript/module/lib/kwik-id-rn-sdk.d.ts +43 -0
- package/lib/typescript/module/lib/kwik-id-rn-sdk.d.ts.map +1 -0
- package/lib/typescript/module/lib/verification-machine.d.ts +78 -0
- package/lib/typescript/module/lib/verification-machine.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/types/events.d.ts +94 -0
- package/lib/typescript/module/types/events.d.ts.map +1 -0
- package/lib/typescript/module/types/index.d.ts +242 -0
- package/lib/typescript/module/types/index.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/KwikIDVerificationModal.d.ts +21 -0
- package/lib/typescript/module/ui/components/KwikIDVerificationModal.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/screens.d.ts +46 -0
- package/lib/typescript/module/ui/components/screens.d.ts.map +1 -0
- package/lib/typescript/module/ui/context/kwik-id-provider.d.ts +19 -0
- package/lib/typescript/module/ui/context/kwik-id-provider.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/use-kwik-verification.d.ts +29 -0
- package/lib/typescript/module/ui/hooks/use-kwik-verification.d.ts.map +1 -0
- package/lib/typescript/module/ui/index.d.ts +7 -0
- package/lib/typescript/module/ui/index.d.ts.map +1 -0
- package/package.json +121 -3
- package/src/api/api-client.ts +319 -0
- package/src/api/api-error.ts +28 -0
- package/src/api/index.ts +12 -0
- package/src/api/upload.ts +87 -0
- package/src/camera/camera-view.tsx +217 -0
- package/src/camera/index.ts +4 -0
- package/src/camera/photo-capture.ts +58 -0
- package/src/crypto/fingerprint.ts +46 -0
- package/src/crypto/hmac.ts +172 -0
- package/src/crypto/index.ts +3 -0
- package/src/crypto/session-manager.ts +200 -0
- package/src/i18n/en.ts +315 -0
- package/src/i18n/fr.ts +325 -0
- package/src/i18n/index.ts +9 -0
- package/src/i18n/locale.ts +15 -0
- package/src/i18n/translator.ts +37 -0
- package/src/index.ts +102 -0
- package/src/lib/device-detector.ts +32 -0
- package/src/lib/event-emitter.ts +90 -0
- package/src/lib/index.ts +15 -0
- package/src/lib/kwik-id-rn-sdk.ts +160 -0
- package/src/lib/verification-machine.ts +273 -0
- package/src/types/events.ts +75 -0
- package/src/types/index.ts +309 -0
- package/src/ui/components/KwikIDVerificationModal.tsx +450 -0
- package/src/ui/components/screens.tsx +283 -0
- package/src/ui/context/kwik-id-provider.tsx +72 -0
- package/src/ui/hooks/use-kwik-verification.ts +121 -0
- package/src/ui/index.ts +9 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multipart upload for RN via expo-file-system (gives real upload progress, which
|
|
3
|
+
* RN's fetch/FormData path cannot). The gateway's multer expects the field `file`.
|
|
4
|
+
*/
|
|
5
|
+
import { FileSystemUploadType, createUploadTask } from "expo-file-system";
|
|
6
|
+
|
|
7
|
+
import type { CaptureAsset } from "../types";
|
|
8
|
+
import { ApiError } from "./api-error";
|
|
9
|
+
|
|
10
|
+
export interface UploadResult {
|
|
11
|
+
uploadId: string;
|
|
12
|
+
status: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type Uploader = (opts: {
|
|
16
|
+
url: string;
|
|
17
|
+
asset: CaptureAsset;
|
|
18
|
+
headers: Record<string, string>;
|
|
19
|
+
onProgress?: (pct: number) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Inactivity timeout in ms: the upload is cancelled and rejected when no progress
|
|
22
|
+
* (or response) arrives for this long. Omit or pass 0 to disable.
|
|
23
|
+
*/
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
}) => Promise<UploadResult>;
|
|
26
|
+
|
|
27
|
+
function parseBody(body: string): { uploadId?: string; status?: string; message?: string } {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(body);
|
|
30
|
+
} catch {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Default uploader — used on device. */
|
|
36
|
+
export const uploadMultipart: Uploader = async ({ url, asset, headers, onProgress, timeoutMs }) => {
|
|
37
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
38
|
+
let rejectTimeout: (error: Error) => void = () => undefined;
|
|
39
|
+
const timedOut = new Promise<never>((_, reject) => {
|
|
40
|
+
rejectTimeout = reject;
|
|
41
|
+
});
|
|
42
|
+
// Reset on every progress event so a slow-but-moving upload isn't killed.
|
|
43
|
+
const armTimeout = () => {
|
|
44
|
+
if (!timeoutMs) return;
|
|
45
|
+
if (timer) clearTimeout(timer);
|
|
46
|
+
timer = setTimeout(() => {
|
|
47
|
+
task.cancelAsync().catch(() => undefined);
|
|
48
|
+
rejectTimeout(new ApiError("Upload timeout", 0, "UPLOAD_TIMEOUT"));
|
|
49
|
+
}, timeoutMs);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const task = createUploadTask(
|
|
53
|
+
url,
|
|
54
|
+
asset.uri,
|
|
55
|
+
{
|
|
56
|
+
httpMethod: "POST",
|
|
57
|
+
uploadType: FileSystemUploadType.MULTIPART,
|
|
58
|
+
fieldName: "file",
|
|
59
|
+
mimeType: asset.type,
|
|
60
|
+
headers,
|
|
61
|
+
},
|
|
62
|
+
(p) => {
|
|
63
|
+
armTimeout();
|
|
64
|
+
if (p.totalBytesExpectedToSend > 0) {
|
|
65
|
+
onProgress?.((p.totalBytesSent / p.totalBytesExpectedToSend) * 100);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
armTimeout();
|
|
71
|
+
let res: Awaited<ReturnType<typeof task.uploadAsync>>;
|
|
72
|
+
try {
|
|
73
|
+
res = await Promise.race([task.uploadAsync(), timedOut]);
|
|
74
|
+
} finally {
|
|
75
|
+
if (timer) clearTimeout(timer);
|
|
76
|
+
}
|
|
77
|
+
if (!res || res.status < 200 || res.status >= 300) {
|
|
78
|
+
const parsed = res ? parseBody(res.body) : {};
|
|
79
|
+
throw new ApiError(
|
|
80
|
+
parsed.message || `Upload failed with status ${res?.status ?? 0}`,
|
|
81
|
+
res?.status ?? 0
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const parsed = parseBody(res.body);
|
|
86
|
+
return { uploadId: parsed.uploadId || "", status: parsed.status || "uploaded" };
|
|
87
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CameraView — vision-camera wrapper for still capture (Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 is capture-only, so this uses `takePhoto` and does NOT run a frame
|
|
5
|
+
* processor (that lands in Phase 2 for liveness). Handles permission + device
|
|
6
|
+
* selection and reports a RawPhoto to the caller.
|
|
7
|
+
*
|
|
8
|
+
* vision-camera is required lazily: its NativeCameraModule throws at import time
|
|
9
|
+
* when the native module is missing (e.g. Expo Go), which would crash any import of
|
|
10
|
+
* this SDK before KwikIdRNSDK.initialize() can report a friendly runtime error.
|
|
11
|
+
*/
|
|
12
|
+
import { useEffect, useRef, useState } from "react";
|
|
13
|
+
|
|
14
|
+
import { ActivityIndicator, Linking, Pressable, StyleSheet, Text, View } from "react-native";
|
|
15
|
+
import type { Camera } from "react-native-vision-camera";
|
|
16
|
+
|
|
17
|
+
import type { RawPhoto } from "./photo-capture";
|
|
18
|
+
|
|
19
|
+
type VisionCamera = typeof import("react-native-vision-camera");
|
|
20
|
+
|
|
21
|
+
let visionCamera: VisionCamera | undefined;
|
|
22
|
+
|
|
23
|
+
function loadVisionCamera(): VisionCamera {
|
|
24
|
+
visionCamera ??= require("react-native-vision-camera") as VisionCamera;
|
|
25
|
+
return visionCamera;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CameraViewProps {
|
|
29
|
+
facing: "front" | "back";
|
|
30
|
+
/** Only render/activate the camera when this view is on screen. */
|
|
31
|
+
active: boolean;
|
|
32
|
+
primaryColor: string;
|
|
33
|
+
/** Optional overlay (e.g. document frame or face oval) drawn over the preview. */
|
|
34
|
+
overlay?: React.ReactNode;
|
|
35
|
+
instruction?: string;
|
|
36
|
+
/** Shown above the shutter when a capture (or the caller's onCapture) fails. */
|
|
37
|
+
captureErrorText?: string;
|
|
38
|
+
/** Permission-denied screen title. */
|
|
39
|
+
permissionTitle?: string;
|
|
40
|
+
/** Permission-denied screen body. */
|
|
41
|
+
permissionBody?: string;
|
|
42
|
+
/** Label of the button that opens the OS settings. */
|
|
43
|
+
openSettingsLabel?: string;
|
|
44
|
+
/** Accessibility label of the shutter button. */
|
|
45
|
+
captureLabel?: string;
|
|
46
|
+
/** May return a promise; a rejection is shown as `captureErrorText` and the user can retry. */
|
|
47
|
+
onCapture: (photo: RawPhoto) => void | Promise<void>;
|
|
48
|
+
onPermissionDenied?: (canAskAgain: boolean) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function CameraView({
|
|
52
|
+
facing,
|
|
53
|
+
active,
|
|
54
|
+
primaryColor,
|
|
55
|
+
overlay,
|
|
56
|
+
instruction,
|
|
57
|
+
captureErrorText = "Could not capture the photo. Please try again.",
|
|
58
|
+
permissionTitle = "Camera access needed",
|
|
59
|
+
permissionBody = "Enable camera access in Settings to continue verification.",
|
|
60
|
+
openSettingsLabel = "Open Settings",
|
|
61
|
+
captureLabel = "Capture photo",
|
|
62
|
+
onCapture,
|
|
63
|
+
onPermissionDenied,
|
|
64
|
+
}: CameraViewProps) {
|
|
65
|
+
const { Camera, useCameraDevice, useCameraPermission } = loadVisionCamera();
|
|
66
|
+
const camera = useRef<Camera>(null);
|
|
67
|
+
const device = useCameraDevice(facing);
|
|
68
|
+
const { hasPermission, requestPermission } = useCameraPermission();
|
|
69
|
+
const [capturing, setCapturing] = useState(false);
|
|
70
|
+
const [permissionChecked, setPermissionChecked] = useState(false);
|
|
71
|
+
const [captureFailed, setCaptureFailed] = useState(false);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
let cancelled = false;
|
|
75
|
+
(async () => {
|
|
76
|
+
if (!hasPermission) {
|
|
77
|
+
const granted = await requestPermission();
|
|
78
|
+
if (!cancelled && !granted) {
|
|
79
|
+
onPermissionDenied?.(false);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!cancelled) setPermissionChecked(true);
|
|
83
|
+
})();
|
|
84
|
+
return () => {
|
|
85
|
+
cancelled = true;
|
|
86
|
+
};
|
|
87
|
+
}, [hasPermission, requestPermission, onPermissionDenied]);
|
|
88
|
+
|
|
89
|
+
const takePhoto = async () => {
|
|
90
|
+
if (!camera.current || capturing) return;
|
|
91
|
+
setCapturing(true);
|
|
92
|
+
setCaptureFailed(false);
|
|
93
|
+
try {
|
|
94
|
+
const photo = await camera.current.takePhoto({ flash: "off" });
|
|
95
|
+
await onCapture({ path: photo.path, width: photo.width, height: photo.height });
|
|
96
|
+
} catch {
|
|
97
|
+
setCaptureFailed(true);
|
|
98
|
+
} finally {
|
|
99
|
+
setCapturing(false);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (!hasPermission && permissionChecked) {
|
|
104
|
+
return (
|
|
105
|
+
<View style={styles.center}>
|
|
106
|
+
<Text style={styles.permTitle}>{permissionTitle}</Text>
|
|
107
|
+
<Text style={styles.permBody}>{permissionBody}</Text>
|
|
108
|
+
<Pressable
|
|
109
|
+
style={[styles.settingsBtn, { borderColor: primaryColor }]}
|
|
110
|
+
onPress={() => Linking.openSettings()}
|
|
111
|
+
>
|
|
112
|
+
<Text style={[styles.settingsText, { color: primaryColor }]}>
|
|
113
|
+
{openSettingsLabel}
|
|
114
|
+
</Text>
|
|
115
|
+
</Pressable>
|
|
116
|
+
</View>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!device || !permissionChecked) {
|
|
121
|
+
return (
|
|
122
|
+
<View style={styles.center}>
|
|
123
|
+
<ActivityIndicator color={primaryColor} />
|
|
124
|
+
</View>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<View style={styles.fill}>
|
|
130
|
+
<Camera
|
|
131
|
+
ref={camera}
|
|
132
|
+
style={StyleSheet.absoluteFill}
|
|
133
|
+
device={device}
|
|
134
|
+
isActive={active}
|
|
135
|
+
photo
|
|
136
|
+
/>
|
|
137
|
+
{overlay}
|
|
138
|
+
<View style={styles.bottomBar}>
|
|
139
|
+
{captureFailed ? (
|
|
140
|
+
<Text
|
|
141
|
+
accessibilityRole="alert"
|
|
142
|
+
style={[styles.instruction, styles.captureError]}
|
|
143
|
+
>
|
|
144
|
+
{captureErrorText}
|
|
145
|
+
</Text>
|
|
146
|
+
) : instruction ? (
|
|
147
|
+
<Text style={styles.instruction}>{instruction}</Text>
|
|
148
|
+
) : null}
|
|
149
|
+
<Pressable
|
|
150
|
+
accessibilityLabel={captureLabel}
|
|
151
|
+
style={styles.shutterOuter}
|
|
152
|
+
onPress={takePhoto}
|
|
153
|
+
disabled={capturing}
|
|
154
|
+
>
|
|
155
|
+
<View style={[styles.shutterInner, capturing && styles.shutterBusy]}>
|
|
156
|
+
{capturing ? <ActivityIndicator color="#fff" /> : null}
|
|
157
|
+
</View>
|
|
158
|
+
</Pressable>
|
|
159
|
+
</View>
|
|
160
|
+
</View>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const styles = StyleSheet.create({
|
|
165
|
+
fill: { flex: 1, backgroundColor: "#000" },
|
|
166
|
+
center: {
|
|
167
|
+
flex: 1,
|
|
168
|
+
alignItems: "center",
|
|
169
|
+
justifyContent: "center",
|
|
170
|
+
padding: 24,
|
|
171
|
+
backgroundColor: "#000",
|
|
172
|
+
},
|
|
173
|
+
bottomBar: {
|
|
174
|
+
position: "absolute",
|
|
175
|
+
bottom: 0,
|
|
176
|
+
left: 0,
|
|
177
|
+
right: 0,
|
|
178
|
+
alignItems: "center",
|
|
179
|
+
paddingBottom: 40,
|
|
180
|
+
},
|
|
181
|
+
instruction: {
|
|
182
|
+
color: "#fff",
|
|
183
|
+
fontSize: 15,
|
|
184
|
+
marginBottom: 16,
|
|
185
|
+
textAlign: "center",
|
|
186
|
+
paddingHorizontal: 24,
|
|
187
|
+
},
|
|
188
|
+
captureError: { color: "#fca5a5", fontWeight: "600" },
|
|
189
|
+
shutterOuter: {
|
|
190
|
+
width: 76,
|
|
191
|
+
height: 76,
|
|
192
|
+
borderRadius: 38,
|
|
193
|
+
borderWidth: 4,
|
|
194
|
+
borderColor: "#fff",
|
|
195
|
+
alignItems: "center",
|
|
196
|
+
justifyContent: "center",
|
|
197
|
+
},
|
|
198
|
+
shutterInner: {
|
|
199
|
+
width: 58,
|
|
200
|
+
height: 58,
|
|
201
|
+
borderRadius: 29,
|
|
202
|
+
backgroundColor: "#fff",
|
|
203
|
+
alignItems: "center",
|
|
204
|
+
justifyContent: "center",
|
|
205
|
+
},
|
|
206
|
+
shutterBusy: { backgroundColor: "#1E3A8A" },
|
|
207
|
+
permTitle: {
|
|
208
|
+
color: "#fff",
|
|
209
|
+
fontSize: 18,
|
|
210
|
+
fontWeight: "600",
|
|
211
|
+
marginBottom: 8,
|
|
212
|
+
textAlign: "center",
|
|
213
|
+
},
|
|
214
|
+
permBody: { color: "#ccc", fontSize: 14, textAlign: "center", marginBottom: 20 },
|
|
215
|
+
settingsBtn: { borderWidth: 1.5, borderRadius: 10, paddingVertical: 12, paddingHorizontal: 24 },
|
|
216
|
+
settingsText: { fontSize: 15, fontWeight: "600" },
|
|
217
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize + compress a captured photo into a CaptureAsset ready for upload.
|
|
3
|
+
*
|
|
4
|
+
* Decoupled from vision-camera: callers pass a plain { path } (the camera hook maps
|
|
5
|
+
* VisionCamera's PhotoFile to this). Keeps the 10 MB server cap in mind by resizing
|
|
6
|
+
* the longest edge and compressing to JPEG.
|
|
7
|
+
*/
|
|
8
|
+
import { SaveFormat, manipulateAsync } from "expo-image-manipulator";
|
|
9
|
+
|
|
10
|
+
import type { CaptureAsset, CaptureType } from "../types";
|
|
11
|
+
|
|
12
|
+
export interface RawPhoto {
|
|
13
|
+
/** Absolute file path or file:// URI from the camera. */
|
|
14
|
+
path: string;
|
|
15
|
+
width?: number;
|
|
16
|
+
height?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MAX_EDGE = 1600;
|
|
20
|
+
|
|
21
|
+
function toFileUri(path: string): string {
|
|
22
|
+
return path.startsWith("file://") || path.startsWith("content://") ? path : `file://${path}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fileName(type: CaptureType, ts: number): string {
|
|
26
|
+
const base = type === "document_front" ? "front" : type === "document_back" ? "back" : "selfie";
|
|
27
|
+
return `${base}_${ts}.jpg`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function compressCapture(
|
|
31
|
+
type: CaptureType,
|
|
32
|
+
photo: RawPhoto,
|
|
33
|
+
timestamp: number
|
|
34
|
+
): Promise<CaptureAsset> {
|
|
35
|
+
const uri = toFileUri(photo.path);
|
|
36
|
+
|
|
37
|
+
// Resize the longest edge to MAX_EDGE (only downscale) and compress to JPEG.
|
|
38
|
+
const longest = Math.max(photo.width ?? 0, photo.height ?? 0);
|
|
39
|
+
const resize =
|
|
40
|
+
longest > MAX_EDGE
|
|
41
|
+
? (photo.width ?? 0) >= (photo.height ?? 0)
|
|
42
|
+
? [{ resize: { width: MAX_EDGE } }]
|
|
43
|
+
: [{ resize: { height: MAX_EDGE } }]
|
|
44
|
+
: [];
|
|
45
|
+
|
|
46
|
+
const result = await manipulateAsync(uri, resize, {
|
|
47
|
+
compress: 0.8,
|
|
48
|
+
format: SaveFormat.JPEG,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
uri: result.uri,
|
|
53
|
+
name: fileName(type, timestamp),
|
|
54
|
+
type: "image/jpeg",
|
|
55
|
+
width: result.width,
|
|
56
|
+
height: result.height,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device fingerprint for RN (replaces the web SDK's canvas/WebGL fingerprint).
|
|
3
|
+
*
|
|
4
|
+
* Built from stable device + locale attributes available without extra permissions.
|
|
5
|
+
* Best-effort: the gateway does not currently require this header.
|
|
6
|
+
*/
|
|
7
|
+
import * as Device from "expo-device";
|
|
8
|
+
import * as Localization from "expo-localization";
|
|
9
|
+
import { Dimensions, Platform } from "react-native";
|
|
10
|
+
|
|
11
|
+
import type { DeviceFingerprint } from "../types";
|
|
12
|
+
import { sha256Hex } from "./hmac";
|
|
13
|
+
|
|
14
|
+
export function generateFingerprint(): DeviceFingerprint {
|
|
15
|
+
const platform = `${Platform.OS} ${String(Platform.Version)}`;
|
|
16
|
+
const osVersion = Device.osVersion ?? String(Platform.Version);
|
|
17
|
+
const deviceModel = [Device.brand, Device.modelName].filter(Boolean).join(" ") || "unknown";
|
|
18
|
+
|
|
19
|
+
const { width, height, scale } = Dimensions.get("screen");
|
|
20
|
+
const screenResolution = `${Math.round(width)}x${Math.round(height)}@${scale}`;
|
|
21
|
+
|
|
22
|
+
const timezone = Localization.getCalendars()[0]?.timeZone ?? "unknown";
|
|
23
|
+
const language = Localization.getLocales()[0]?.languageTag ?? "unknown";
|
|
24
|
+
|
|
25
|
+
const components = [
|
|
26
|
+
platform,
|
|
27
|
+
osVersion,
|
|
28
|
+
deviceModel,
|
|
29
|
+
screenResolution,
|
|
30
|
+
timezone,
|
|
31
|
+
language,
|
|
32
|
+
Device.osName ?? "",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const fingerprintHash = sha256Hex(components.join("|"));
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
platform,
|
|
39
|
+
osVersion,
|
|
40
|
+
deviceModel,
|
|
41
|
+
screenResolution,
|
|
42
|
+
timezone,
|
|
43
|
+
language,
|
|
44
|
+
fingerprintHash,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-JS SHA-256 and HMAC-SHA256.
|
|
3
|
+
*
|
|
4
|
+
* Self-contained (no native dependency) so it runs identically on device and in
|
|
5
|
+
* node tests, and never blocks the verification flow. The web SDK used
|
|
6
|
+
* `crypto.subtle`; expo-crypto exposes digests but not raw HMAC, so we implement
|
|
7
|
+
* the standard ipad/opad construction here. Signatures are best-effort — the
|
|
8
|
+
* KwikId gateway enforces only the Bearer token.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const K = [
|
|
12
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
13
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
14
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
15
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
16
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
17
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
18
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
19
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function rotr(x: number, n: number): number {
|
|
23
|
+
return (x >>> n) | (x << (32 - n));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** SHA-256 over raw bytes → 32-byte digest. */
|
|
27
|
+
export function sha256Bytes(message: Uint8Array): Uint8Array {
|
|
28
|
+
const h = [
|
|
29
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
|
|
30
|
+
0x5be0cd19,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Pre-processing (padding).
|
|
34
|
+
const bitLen = message.length * 8;
|
|
35
|
+
const withOne = message.length + 1;
|
|
36
|
+
const totalLen = withOne + ((56 - (withOne % 64) + 64) % 64) + 8;
|
|
37
|
+
const padded = new Uint8Array(totalLen);
|
|
38
|
+
padded.set(message);
|
|
39
|
+
padded[message.length] = 0x80;
|
|
40
|
+
// Append 64-bit big-endian length (top 32 bits assumed 0 for our payload sizes).
|
|
41
|
+
const dv = new DataView(padded.buffer);
|
|
42
|
+
dv.setUint32(totalLen - 4, bitLen >>> 0, false);
|
|
43
|
+
dv.setUint32(totalLen - 8, Math.floor(bitLen / 0x100000000), false);
|
|
44
|
+
|
|
45
|
+
const w = new Uint32Array(64);
|
|
46
|
+
for (let offset = 0; offset < totalLen; offset += 64) {
|
|
47
|
+
for (let i = 0; i < 16; i++) {
|
|
48
|
+
w[i] = dv.getUint32(offset + i * 4, false);
|
|
49
|
+
}
|
|
50
|
+
for (let i = 16; i < 64; i++) {
|
|
51
|
+
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
|
|
52
|
+
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
|
|
53
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
57
|
+
for (let i = 0; i < 64; i++) {
|
|
58
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
59
|
+
const ch = (e & f) ^ (~e & g);
|
|
60
|
+
const temp1 = (hh + S1 + ch + K[i] + w[i]) >>> 0;
|
|
61
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
62
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
63
|
+
const temp2 = (S0 + maj) >>> 0;
|
|
64
|
+
hh = g;
|
|
65
|
+
g = f;
|
|
66
|
+
f = e;
|
|
67
|
+
e = (d + temp1) >>> 0;
|
|
68
|
+
d = c;
|
|
69
|
+
c = b;
|
|
70
|
+
b = a;
|
|
71
|
+
a = (temp1 + temp2) >>> 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
h[0] = (h[0] + a) >>> 0;
|
|
75
|
+
h[1] = (h[1] + b) >>> 0;
|
|
76
|
+
h[2] = (h[2] + c) >>> 0;
|
|
77
|
+
h[3] = (h[3] + d) >>> 0;
|
|
78
|
+
h[4] = (h[4] + e) >>> 0;
|
|
79
|
+
h[5] = (h[5] + f) >>> 0;
|
|
80
|
+
h[6] = (h[6] + g) >>> 0;
|
|
81
|
+
h[7] = (h[7] + hh) >>> 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const out = new Uint8Array(32);
|
|
85
|
+
const outDv = new DataView(out.buffer);
|
|
86
|
+
for (let i = 0; i < 8; i++) outDv.setUint32(i * 4, h[i], false);
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function utf8Bytes(str: string): Uint8Array {
|
|
91
|
+
// Minimal UTF-8 encoder (TextEncoder may be absent in some RN engines).
|
|
92
|
+
const bytes: number[] = [];
|
|
93
|
+
for (let i = 0; i < str.length; i++) {
|
|
94
|
+
let code = str.charCodeAt(i);
|
|
95
|
+
if (code < 0x80) {
|
|
96
|
+
bytes.push(code);
|
|
97
|
+
} else if (code < 0x800) {
|
|
98
|
+
bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
|
|
99
|
+
} else if (code >= 0xd800 && code <= 0xdbff) {
|
|
100
|
+
// Surrogate pair.
|
|
101
|
+
const hi = code;
|
|
102
|
+
const lo = str.charCodeAt(++i);
|
|
103
|
+
code = 0x10000 + ((hi - 0xd800) << 10) + (lo - 0xdc00);
|
|
104
|
+
bytes.push(
|
|
105
|
+
0xf0 | (code >> 18),
|
|
106
|
+
0x80 | ((code >> 12) & 0x3f),
|
|
107
|
+
0x80 | ((code >> 6) & 0x3f),
|
|
108
|
+
0x80 | (code & 0x3f)
|
|
109
|
+
);
|
|
110
|
+
} else {
|
|
111
|
+
bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return new Uint8Array(bytes);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function toHex(bytes: Uint8Array): string {
|
|
118
|
+
let hex = "";
|
|
119
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
120
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
121
|
+
}
|
|
122
|
+
return hex;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** SHA-256 of a UTF-8 string → lowercase hex. */
|
|
126
|
+
export function sha256Hex(message: string): string {
|
|
127
|
+
return toHex(sha256Bytes(utf8Bytes(message)));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const BLOCK_SIZE = 64;
|
|
131
|
+
|
|
132
|
+
/** HMAC-SHA256(message, key) → lowercase hex. */
|
|
133
|
+
export function hmacSha256Hex(message: string, key: string): string {
|
|
134
|
+
let keyBytes = utf8Bytes(key);
|
|
135
|
+
if (keyBytes.length > BLOCK_SIZE) {
|
|
136
|
+
keyBytes = sha256Bytes(keyBytes);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const block = new Uint8Array(BLOCK_SIZE);
|
|
140
|
+
block.set(keyBytes);
|
|
141
|
+
|
|
142
|
+
const ipad = new Uint8Array(BLOCK_SIZE);
|
|
143
|
+
const opad = new Uint8Array(BLOCK_SIZE);
|
|
144
|
+
for (let i = 0; i < BLOCK_SIZE; i++) {
|
|
145
|
+
ipad[i] = block[i] ^ 0x36;
|
|
146
|
+
opad[i] = block[i] ^ 0x5c;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const msgBytes = utf8Bytes(message);
|
|
150
|
+
const inner = new Uint8Array(BLOCK_SIZE + msgBytes.length);
|
|
151
|
+
inner.set(ipad);
|
|
152
|
+
inner.set(msgBytes, BLOCK_SIZE);
|
|
153
|
+
const innerHash = sha256Bytes(inner);
|
|
154
|
+
|
|
155
|
+
const outer = new Uint8Array(BLOCK_SIZE + innerHash.length);
|
|
156
|
+
outer.set(opad);
|
|
157
|
+
outer.set(innerHash, BLOCK_SIZE);
|
|
158
|
+
|
|
159
|
+
return toHex(sha256Bytes(outer));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Generate a hex nonce. Deterministic seed optional for tests. */
|
|
163
|
+
export function generateNonce(length = 32, seed?: () => number): string {
|
|
164
|
+
const rand = seed ?? Math.random;
|
|
165
|
+
let hex = "";
|
|
166
|
+
for (let i = 0; i < length; i++) {
|
|
167
|
+
hex += Math.floor(rand() * 256)
|
|
168
|
+
.toString(16)
|
|
169
|
+
.padStart(2, "0");
|
|
170
|
+
}
|
|
171
|
+
return hex;
|
|
172
|
+
}
|