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.
Files changed (47) hide show
  1. package/README.md +185 -0
  2. package/dist/adapters/blazeface.d.ts +15 -0
  3. package/dist/adapters/blazeface.js +258 -0
  4. package/dist/adapters/mediapipe.d.ts +7 -0
  5. package/dist/adapters/mediapipe.js +55 -0
  6. package/dist/adapters/onnx.d.ts +10 -0
  7. package/dist/adapters/onnx.js +171 -0
  8. package/dist/camera.d.ts +15 -0
  9. package/dist/camera.js +76 -0
  10. package/dist/embedding/descriptor.d.ts +22 -0
  11. package/dist/embedding/descriptor.js +134 -0
  12. package/dist/hashing/facehash.d.ts +3 -0
  13. package/dist/hashing/facehash.js +27 -0
  14. package/dist/hashing/webcrypto.d.ts +1 -0
  15. package/dist/hashing/webcrypto.js +1 -0
  16. package/dist/index.d.ts +6 -0
  17. package/dist/index.js +7 -0
  18. package/dist/liveness/challenges.d.ts +3 -0
  19. package/dist/liveness/challenges.js +34 -0
  20. package/dist/liveness/scorer.d.ts +1 -0
  21. package/dist/liveness/scorer.js +3 -0
  22. package/dist/liveness/texture.d.ts +72 -0
  23. package/dist/liveness/texture.js +266 -0
  24. package/dist/types.d.ts +86 -0
  25. package/dist/types.js +9 -0
  26. package/dist/verify.d.ts +4 -0
  27. package/dist/verify.js +892 -0
  28. package/dist/worker/frame.d.ts +5 -0
  29. package/dist/worker/frame.js +1 -0
  30. package/dist/worker/infer.d.ts +4 -0
  31. package/dist/worker/infer.js +22 -0
  32. package/dist/worker/worker.d.ts +0 -0
  33. package/dist/worker/worker.js +61 -0
  34. package/package.json +46 -0
  35. package/public/models/age_gender.onnx +1446 -0
  36. package/public/models/age_gender_model-weights_manifest.json +62 -0
  37. package/public/models/age_gender_model.shard1 +1447 -0
  38. package/public/models/face_landmark_68_model-weights_manifest.json +60 -0
  39. package/public/models/face_landmark_68_model.shard1 +1447 -0
  40. package/public/models/face_recognition_model-weights_manifest.json +128 -0
  41. package/public/models/face_recognition_model.shard1 +1447 -0
  42. package/public/models/face_recognition_model.shard2 +1447 -0
  43. package/public/models/ort-wasm-simd-threaded.asyncify.wasm +0 -0
  44. package/public/models/ort-wasm-simd-threaded.jsep.wasm +0 -0
  45. package/public/models/ort-wasm-simd-threaded.wasm +0 -0
  46. package/public/models/tiny_face_detector_model-weights_manifest.json +30 -0
  47. 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
+ }
@@ -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
@@ -0,0 +1,9 @@
1
+ export const DEFAULT_CONFIG = {
2
+ challenges: [],
3
+ minLivenessScore: 0.85,
4
+ minAgeConfidence: 0.65,
5
+ minAgeEstimate: 18,
6
+ timeoutMs: 90000,
7
+ maxRetries: 3,
8
+ cooldownMinutes: 15
9
+ };
@@ -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;