solana-age-verify-sdk 2.0.0-beta.2
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/README.md +185 -0
- package/dist/adapters/blazeface.d.ts +15 -0
- package/dist/adapters/blazeface.js +258 -0
- package/dist/adapters/mediapipe.d.ts +7 -0
- package/dist/adapters/mediapipe.js +55 -0
- package/dist/adapters/onnx.d.ts +10 -0
- package/dist/adapters/onnx.js +171 -0
- package/dist/camera.d.ts +15 -0
- package/dist/camera.js +76 -0
- package/dist/embedding/descriptor.d.ts +22 -0
- package/dist/embedding/descriptor.js +134 -0
- package/dist/hashing/facehash.d.ts +3 -0
- package/dist/hashing/facehash.js +27 -0
- package/dist/hashing/webcrypto.d.ts +1 -0
- package/dist/hashing/webcrypto.js +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/liveness/challenges.d.ts +3 -0
- package/dist/liveness/challenges.js +34 -0
- package/dist/liveness/scorer.d.ts +1 -0
- package/dist/liveness/scorer.js +3 -0
- package/dist/liveness/texture.d.ts +72 -0
- package/dist/liveness/texture.js +266 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.js +9 -0
- package/dist/verify.d.ts +4 -0
- package/dist/verify.js +892 -0
- package/dist/worker/frame.d.ts +5 -0
- package/dist/worker/frame.js +1 -0
- package/dist/worker/infer.d.ts +4 -0
- package/dist/worker/infer.js +22 -0
- package/dist/worker/worker.d.ts +0 -0
- package/dist/worker/worker.js +61 -0
- package/package.json +46 -0
- package/public/models/age_gender.onnx +1446 -0
- package/public/models/age_gender_model-weights_manifest.json +62 -0
- package/public/models/age_gender_model.shard1 +1447 -0
- package/public/models/face_landmark_68_model-weights_manifest.json +60 -0
- package/public/models/face_landmark_68_model.shard1 +1447 -0
- package/public/models/face_recognition_model-weights_manifest.json +128 -0
- package/public/models/face_recognition_model.shard1 +1447 -0
- package/public/models/face_recognition_model.shard2 +1447 -0
- package/public/models/ort-wasm-simd-threaded.asyncify.wasm +0 -0
- package/public/models/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/public/models/ort-wasm-simd-threaded.wasm +0 -0
- package/public/models/tiny_face_detector_model-weights_manifest.json +30 -0
- package/public/models/tiny_face_detector_model.shard1 +1447 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Texture Analysis for Passive Liveness Detection
|
|
3
|
+
*
|
|
4
|
+
* This module analyzes the surface characteristics of a face to distinguish between:
|
|
5
|
+
* - Real human faces (3D, natural skin texture, pores, wrinkles)
|
|
6
|
+
* - Spoofing attempts (2D photos, screens, masks)
|
|
7
|
+
*
|
|
8
|
+
* Techniques used:
|
|
9
|
+
* 1. Local Binary Patterns (LBP) - Detects local texture patterns
|
|
10
|
+
* 2. Frequency Analysis - Identifies print/screen artifacts
|
|
11
|
+
* 3. Moiré Pattern Detection - Detects screen recapture
|
|
12
|
+
* 4. Reflectance Analysis - Analyzes light interaction with surface
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Analyzes texture from a face region to detect liveness
|
|
16
|
+
*/
|
|
17
|
+
export class TextureAnalyzer {
|
|
18
|
+
constructor() {
|
|
19
|
+
Object.defineProperty(this, "canvas", {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value: void 0
|
|
24
|
+
});
|
|
25
|
+
Object.defineProperty(this, "ctx", {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
configurable: true,
|
|
28
|
+
writable: true,
|
|
29
|
+
value: void 0
|
|
30
|
+
});
|
|
31
|
+
// Use OffscreenCanvas in worker, HTMLCanvasElement in main thread
|
|
32
|
+
if (typeof OffscreenCanvas !== 'undefined') {
|
|
33
|
+
this.canvas = new OffscreenCanvas(256, 256);
|
|
34
|
+
this.ctx = this.canvas.getContext('2d');
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
this.canvas = document.createElement('canvas');
|
|
38
|
+
this.canvas.width = 256;
|
|
39
|
+
this.canvas.height = 256;
|
|
40
|
+
this.ctx = this.canvas.getContext('2d');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Main analysis function
|
|
45
|
+
*/
|
|
46
|
+
async analyze(frame, faceRegion) {
|
|
47
|
+
// Extract face region or use entire frame
|
|
48
|
+
const imageData = this.extractRegion(frame, faceRegion);
|
|
49
|
+
// Run parallel analyses
|
|
50
|
+
const [lbpScore, freqScore, moireDetected, reflectance] = await Promise.all([
|
|
51
|
+
this.analyzeLBP(imageData),
|
|
52
|
+
this.analyzeFrequency(imageData),
|
|
53
|
+
this.detectMoire(imageData),
|
|
54
|
+
this.analyzeReflectance(imageData)
|
|
55
|
+
]);
|
|
56
|
+
// Combine scores
|
|
57
|
+
const skinComplexity = lbpScore;
|
|
58
|
+
const frequencyScore = freqScore;
|
|
59
|
+
// Decision logic: Real if complexity is high AND no moiré AND natural reflectance
|
|
60
|
+
const complexityThreshold = 0.3;
|
|
61
|
+
const frequencyThreshold = 0.35;
|
|
62
|
+
const isReal = skinComplexity > complexityThreshold &&
|
|
63
|
+
frequencyScore > frequencyThreshold &&
|
|
64
|
+
!moireDetected &&
|
|
65
|
+
reflectance !== 'artificial';
|
|
66
|
+
// Confidence based on how far from thresholds
|
|
67
|
+
const complexityDist = Math.abs(skinComplexity - complexityThreshold);
|
|
68
|
+
const freqDist = Math.abs(frequencyScore - frequencyThreshold);
|
|
69
|
+
const confidence = Math.min(1.0, (complexityDist + freqDist) / 2 + 0.5);
|
|
70
|
+
return {
|
|
71
|
+
isReal,
|
|
72
|
+
confidence,
|
|
73
|
+
features: {
|
|
74
|
+
skinComplexity,
|
|
75
|
+
moireDetected,
|
|
76
|
+
frequencyScore,
|
|
77
|
+
reflectancePattern: reflectance
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Extract region of interest from frame
|
|
83
|
+
*/
|
|
84
|
+
extractRegion(frame, region) {
|
|
85
|
+
// Draw to canvas
|
|
86
|
+
if (frame instanceof ImageData) {
|
|
87
|
+
this.ctx.putImageData(frame, 0, 0);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
91
|
+
}
|
|
92
|
+
// Extract region if specified
|
|
93
|
+
if (region) {
|
|
94
|
+
const { x, y, width, height } = region;
|
|
95
|
+
return this.ctx.getImageData(x, y, width, height);
|
|
96
|
+
}
|
|
97
|
+
return this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Local Binary Patterns (LBP) Analysis
|
|
101
|
+
* Detects micro-texture patterns in skin
|
|
102
|
+
*/
|
|
103
|
+
async analyzeLBP(imageData) {
|
|
104
|
+
const { data, width, height } = imageData;
|
|
105
|
+
const grayscale = this.toGrayscale(data, width, height);
|
|
106
|
+
// LBP histogram (256 bins for uniform LBP)
|
|
107
|
+
const histogram = new Array(256).fill(0);
|
|
108
|
+
// Calculate LBP for each pixel (excluding borders)
|
|
109
|
+
for (let y = 1; y < height - 1; y++) {
|
|
110
|
+
for (let x = 1; x < width - 1; x++) {
|
|
111
|
+
const center = grayscale[y * width + x];
|
|
112
|
+
let lbpValue = 0;
|
|
113
|
+
// 8 neighbors in clockwise order
|
|
114
|
+
const neighbors = [
|
|
115
|
+
grayscale[(y - 1) * width + (x - 1)], // top-left
|
|
116
|
+
grayscale[(y - 1) * width + x], // top
|
|
117
|
+
grayscale[(y - 1) * width + (x + 1)], // top-right
|
|
118
|
+
grayscale[y * width + (x + 1)], // right
|
|
119
|
+
grayscale[(y + 1) * width + (x + 1)], // bottom-right
|
|
120
|
+
grayscale[(y + 1) * width + x], // bottom
|
|
121
|
+
grayscale[(y + 1) * width + (x - 1)], // bottom-left
|
|
122
|
+
grayscale[y * width + (x - 1)] // left
|
|
123
|
+
];
|
|
124
|
+
// Build LBP code
|
|
125
|
+
for (let i = 0; i < 8; i++) {
|
|
126
|
+
if (neighbors[i] >= center) {
|
|
127
|
+
lbpValue |= (1 << i);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
histogram[lbpValue]++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Normalize histogram
|
|
134
|
+
const totalPixels = (width - 2) * (height - 2);
|
|
135
|
+
const normalizedHist = histogram.map(v => v / totalPixels);
|
|
136
|
+
// Calculate entropy (measure of texture complexity)
|
|
137
|
+
let entropy = 0;
|
|
138
|
+
for (const p of normalizedHist) {
|
|
139
|
+
if (p > 0) {
|
|
140
|
+
entropy -= p * Math.log2(p);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Normalize entropy to 0-1 range (max entropy for 256 bins is 8)
|
|
144
|
+
const normalizedEntropy = entropy / 8;
|
|
145
|
+
// Real skin has moderate-to-high entropy (complex texture)
|
|
146
|
+
// Photos/screens have lower entropy (uniform patterns)
|
|
147
|
+
return normalizedEntropy;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Frequency Domain Analysis
|
|
151
|
+
* Detects print patterns and screen grids
|
|
152
|
+
*/
|
|
153
|
+
async analyzeFrequency(imageData) {
|
|
154
|
+
const { data, width, height } = imageData;
|
|
155
|
+
const grayscale = this.toGrayscale(data, width, height);
|
|
156
|
+
// Simple frequency analysis using gradient magnitude
|
|
157
|
+
let totalGradient = 0;
|
|
158
|
+
let highFreqCount = 0;
|
|
159
|
+
for (let y = 1; y < height - 1; y++) {
|
|
160
|
+
for (let x = 1; x < width - 1; x++) {
|
|
161
|
+
// Sobel gradients
|
|
162
|
+
const gx = -grayscale[(y - 1) * width + (x - 1)] +
|
|
163
|
+
grayscale[(y - 1) * width + (x + 1)] +
|
|
164
|
+
-2 * grayscale[y * width + (x - 1)] +
|
|
165
|
+
2 * grayscale[y * width + (x + 1)] +
|
|
166
|
+
-grayscale[(y + 1) * width + (x - 1)] +
|
|
167
|
+
grayscale[(y + 1) * width + (x + 1)];
|
|
168
|
+
const gy = -grayscale[(y - 1) * width + (x - 1)] +
|
|
169
|
+
-2 * grayscale[(y - 1) * width + x] +
|
|
170
|
+
-grayscale[(y - 1) * width + (x + 1)] +
|
|
171
|
+
grayscale[(y + 1) * width + (x - 1)] +
|
|
172
|
+
2 * grayscale[(y + 1) * width + x] +
|
|
173
|
+
grayscale[(y + 1) * width + (x + 1)];
|
|
174
|
+
const magnitude = Math.sqrt(gx * gx + gy * gy);
|
|
175
|
+
totalGradient += magnitude;
|
|
176
|
+
// Count high-frequency components (sharp edges)
|
|
177
|
+
if (magnitude > 50) {
|
|
178
|
+
highFreqCount++;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const avgGradient = totalGradient / ((width - 2) * (height - 2));
|
|
183
|
+
const highFreqRatio = highFreqCount / ((width - 2) * (height - 2));
|
|
184
|
+
// Real skin has moderate gradients with natural variation
|
|
185
|
+
// Printed images have either too uniform or too sharp edges
|
|
186
|
+
// Score based on "naturalness" of gradient distribution
|
|
187
|
+
const naturalGradientRange = avgGradient > 5 && avgGradient < 30;
|
|
188
|
+
const naturalFreqRatio = highFreqRatio > 0.01 && highFreqRatio < 0.15;
|
|
189
|
+
return (naturalGradientRange && naturalFreqRatio) ? 0.7 : 0.3;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Moiré Pattern Detection
|
|
193
|
+
* Detects interference patterns from screen recapture
|
|
194
|
+
*/
|
|
195
|
+
async detectMoire(imageData) {
|
|
196
|
+
const { data, width, height } = imageData;
|
|
197
|
+
// Look for periodic patterns that indicate moiré
|
|
198
|
+
// Simple approach: check for regular oscillations in intensity
|
|
199
|
+
let periodicityScore = 0;
|
|
200
|
+
const sampleRows = 10;
|
|
201
|
+
for (let i = 0; i < sampleRows; i++) {
|
|
202
|
+
const y = Math.floor((height / sampleRows) * i);
|
|
203
|
+
const rowData = [];
|
|
204
|
+
for (let x = 0; x < width; x++) {
|
|
205
|
+
const idx = (y * width + x) * 4;
|
|
206
|
+
const gray = (data[idx] + data[idx + 1] + data[idx + 2]) / 3;
|
|
207
|
+
rowData.push(gray);
|
|
208
|
+
}
|
|
209
|
+
// Count zero-crossings (sign changes in derivative)
|
|
210
|
+
let crossings = 0;
|
|
211
|
+
for (let x = 1; x < rowData.length - 1; x++) {
|
|
212
|
+
const diff1 = rowData[x] - rowData[x - 1];
|
|
213
|
+
const diff2 = rowData[x + 1] - rowData[x];
|
|
214
|
+
if (diff1 * diff2 < 0)
|
|
215
|
+
crossings++;
|
|
216
|
+
}
|
|
217
|
+
// High crossing count indicates periodic pattern
|
|
218
|
+
const crossingRatio = crossings / width;
|
|
219
|
+
if (crossingRatio > 0.3)
|
|
220
|
+
periodicityScore++;
|
|
221
|
+
}
|
|
222
|
+
// Moiré detected if multiple rows show high periodicity
|
|
223
|
+
return periodicityScore > sampleRows * 0.5;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Reflectance Pattern Analysis
|
|
227
|
+
* Analyzes how light interacts with the surface
|
|
228
|
+
*/
|
|
229
|
+
async analyzeReflectance(imageData) {
|
|
230
|
+
const { data } = imageData;
|
|
231
|
+
// Analyze brightness distribution
|
|
232
|
+
const brightnesses = [];
|
|
233
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
234
|
+
const brightness = (data[i] + data[i + 1] + data[i + 2]) / 3;
|
|
235
|
+
brightnesses.push(brightness);
|
|
236
|
+
}
|
|
237
|
+
// Calculate statistics
|
|
238
|
+
const mean = brightnesses.reduce((a, b) => a + b, 0) / brightnesses.length;
|
|
239
|
+
const variance = brightnesses.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / brightnesses.length;
|
|
240
|
+
const stdDev = Math.sqrt(variance);
|
|
241
|
+
// Real skin has moderate variance (natural shadows and highlights)
|
|
242
|
+
// Photos/screens tend to have either too uniform or too high contrast
|
|
243
|
+
const coefficientOfVariation = stdDev / (mean + 1); // +1 to avoid division by zero
|
|
244
|
+
if (coefficientOfVariation > 0.15 && coefficientOfVariation < 0.5) {
|
|
245
|
+
return 'natural';
|
|
246
|
+
}
|
|
247
|
+
else if (coefficientOfVariation < 0.1 || coefficientOfVariation > 0.7) {
|
|
248
|
+
return 'artificial';
|
|
249
|
+
}
|
|
250
|
+
return 'unknown';
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Convert RGBA to grayscale
|
|
254
|
+
*/
|
|
255
|
+
toGrayscale(data, width, height) {
|
|
256
|
+
const grayscale = new Uint8Array(width * height);
|
|
257
|
+
for (let i = 0; i < grayscale.length; i++) {
|
|
258
|
+
const idx = i * 4;
|
|
259
|
+
// Luminance formula
|
|
260
|
+
grayscale[i] = Math.floor(0.299 * data[idx] +
|
|
261
|
+
0.587 * data[idx + 1] +
|
|
262
|
+
0.114 * data[idx + 2]);
|
|
263
|
+
}
|
|
264
|
+
return grayscale;
|
|
265
|
+
}
|
|
266
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export interface VerifyHost18PlusOptions {
|
|
2
|
+
walletPubkeyBase58: string;
|
|
3
|
+
videoElement?: HTMLVideoElement;
|
|
4
|
+
uiMountEl?: HTMLElement;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
config?: Partial<VerifyConfig>;
|
|
7
|
+
onChallenge?: (challenge: string) => void;
|
|
8
|
+
modelPath?: string;
|
|
9
|
+
workerFactory?: () => Worker;
|
|
10
|
+
connection?: any;
|
|
11
|
+
wallet?: {
|
|
12
|
+
publicKey: any;
|
|
13
|
+
signTransaction: (tx: any) => Promise<any>;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface VerifyConfig {
|
|
17
|
+
challenges: string[];
|
|
18
|
+
minLivenessScore: number;
|
|
19
|
+
minAgeConfidence: number;
|
|
20
|
+
minAgeEstimate: number;
|
|
21
|
+
timeoutMs: number;
|
|
22
|
+
maxRetries: number;
|
|
23
|
+
cooldownMinutes: number;
|
|
24
|
+
}
|
|
25
|
+
export declare const DEFAULT_CONFIG: VerifyConfig;
|
|
26
|
+
export interface ChallengeResult {
|
|
27
|
+
type: string;
|
|
28
|
+
passed: boolean;
|
|
29
|
+
score: number;
|
|
30
|
+
}
|
|
31
|
+
export interface VerifyResult {
|
|
32
|
+
over18: boolean;
|
|
33
|
+
facehash: string;
|
|
34
|
+
description: string;
|
|
35
|
+
verifiedAt: string;
|
|
36
|
+
protocolFeePaid?: boolean;
|
|
37
|
+
protocolFeeTxId?: string;
|
|
38
|
+
evidence: {
|
|
39
|
+
ageEstimate: number;
|
|
40
|
+
ageConfidence: number;
|
|
41
|
+
livenessScore: number;
|
|
42
|
+
textureScore?: number;
|
|
43
|
+
textureFeatures?: TextureFeatures;
|
|
44
|
+
ageMethod?: string;
|
|
45
|
+
challenges: ChallengeResult[];
|
|
46
|
+
modelVersions: Record<string, string>;
|
|
47
|
+
saltHex: string;
|
|
48
|
+
sessionNonceHex: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export interface FrameData {
|
|
52
|
+
data: Uint8ClampedArray;
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
timestamp: number;
|
|
56
|
+
}
|
|
57
|
+
export interface WorkerResponse {
|
|
58
|
+
type: 'RESULT' | 'ERROR' | 'LOADED';
|
|
59
|
+
payload?: any;
|
|
60
|
+
error?: string;
|
|
61
|
+
}
|
|
62
|
+
export interface WorkerRequest {
|
|
63
|
+
type: 'PROCESS_FRAME' | 'LOAD_MODELS';
|
|
64
|
+
payload?: any;
|
|
65
|
+
}
|
|
66
|
+
export interface TextureFeatures {
|
|
67
|
+
skinComplexity: number;
|
|
68
|
+
moireDetected: boolean;
|
|
69
|
+
frequencyScore: number;
|
|
70
|
+
reflectancePattern: 'natural' | 'artificial' | 'unknown';
|
|
71
|
+
}
|
|
72
|
+
export interface DetectionResult {
|
|
73
|
+
faceFound: boolean;
|
|
74
|
+
landmarks?: number[];
|
|
75
|
+
ageEstimate?: number;
|
|
76
|
+
embedding?: number[];
|
|
77
|
+
confidence?: number;
|
|
78
|
+
textureScore?: number;
|
|
79
|
+
textureFeatures?: TextureFeatures;
|
|
80
|
+
ageConfidence?: number;
|
|
81
|
+
ageMethod?: 'onnx' | 'geometric' | 'unknown';
|
|
82
|
+
}
|
|
83
|
+
export interface FaceModelAdapter {
|
|
84
|
+
load(basePath?: string): Promise<void>;
|
|
85
|
+
detect(frame: ImageData | HTMLCanvasElement | OffscreenCanvas): Promise<DetectionResult>;
|
|
86
|
+
}
|
package/dist/types.js
ADDED
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { VerifyHost18PlusOptions, VerifyResult } from './types';
|
|
2
|
+
export declare function verifyHost18Plus(options: VerifyHost18PlusOptions): Promise<VerifyResult>;
|
|
3
|
+
export declare function createVerificationUI(): HTMLElement;
|
|
4
|
+
export declare function setExecutionBackend(backend: 'tfjs' | 'onnx'): void;
|