@duvdu-v1/duvdu 1.1.359 → 1.1.361
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/build/index.d.ts +1 -1
- package/build/index.js +1 -1
- package/build/mailer/mailer.service.d.ts +4 -3
- package/build/mailer/mailer.service.js +17 -11
- package/build/models/User.model.js +8 -10
- package/build/utils/{bucket.d.ts → bucket-wasabi.d.ts} +10 -9
- package/build/utils/{bucket.js → bucket-wasabi.js} +102 -124
- package/package.json +1 -1
package/build/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export * from './errors/not-allowed-error';
|
|
|
9
9
|
export * from './errors/pdf-generation-error';
|
|
10
10
|
export * from './utils/api-feature';
|
|
11
11
|
export * from './utils/generateToken';
|
|
12
|
-
export * from './utils/bucket';
|
|
12
|
+
export * from './utils/bucket-wasabi';
|
|
13
13
|
export * from './utils/file';
|
|
14
14
|
export * from './utils/date';
|
|
15
15
|
export * from './utils/mask';
|
package/build/index.js
CHANGED
|
@@ -25,7 +25,7 @@ __exportStar(require("./errors/not-allowed-error"), exports);
|
|
|
25
25
|
__exportStar(require("./errors/pdf-generation-error"), exports);
|
|
26
26
|
__exportStar(require("./utils/api-feature"), exports);
|
|
27
27
|
__exportStar(require("./utils/generateToken"), exports);
|
|
28
|
-
__exportStar(require("./utils/bucket"), exports);
|
|
28
|
+
__exportStar(require("./utils/bucket-wasabi"), exports);
|
|
29
29
|
__exportStar(require("./utils/file"), exports);
|
|
30
30
|
__exportStar(require("./utils/date"), exports);
|
|
31
31
|
__exportStar(require("./utils/mask"), exports);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { EmailJobData, EmailTemplate } from './mailer.interface';
|
|
2
2
|
declare class MailerServiceClass {
|
|
3
|
-
private
|
|
4
|
-
|
|
5
|
-
private
|
|
3
|
+
private _strategy;
|
|
4
|
+
private strategyInitialized;
|
|
5
|
+
private get strategy();
|
|
6
|
+
private buildStrategy;
|
|
6
7
|
private renderTemplate;
|
|
7
8
|
sendMail<T extends EmailTemplate>(options: EmailJobData<T>): Promise<void>;
|
|
8
9
|
}
|
|
@@ -50,26 +50,31 @@ const resend_strategy_1 = require("./strategies/resend.strategy");
|
|
|
50
50
|
const smtp_strategy_1 = require("./strategies/smtp.strategy");
|
|
51
51
|
class MailerServiceClass {
|
|
52
52
|
constructor() {
|
|
53
|
-
this.
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
this._strategy = null;
|
|
54
|
+
this.strategyInitialized = false;
|
|
55
|
+
}
|
|
56
|
+
get strategy() {
|
|
57
|
+
if (!this.strategyInitialized) {
|
|
58
|
+
this.strategyInitialized = true;
|
|
59
|
+
this._strategy = this.buildStrategy();
|
|
56
60
|
}
|
|
61
|
+
return this._strategy;
|
|
57
62
|
}
|
|
58
|
-
|
|
63
|
+
buildStrategy() {
|
|
64
|
+
if (!process.env.SEND_MAIL)
|
|
65
|
+
return null;
|
|
59
66
|
const driver = process.env.MAIL_DRIVER;
|
|
60
67
|
/* eslint-disable indent */
|
|
61
68
|
switch (driver) {
|
|
62
69
|
case 'smtp':
|
|
63
|
-
this.strategy = new smtp_strategy_1.SmtpStrategy();
|
|
64
70
|
console.log('Mailer strategy set to SMTP');
|
|
65
|
-
|
|
71
|
+
return new smtp_strategy_1.SmtpStrategy();
|
|
66
72
|
case 'resend':
|
|
67
|
-
this.strategy = new resend_strategy_1.ResendStrategy();
|
|
68
73
|
console.log('Mailer strategy set to Resend');
|
|
69
|
-
|
|
74
|
+
return new resend_strategy_1.ResendStrategy();
|
|
70
75
|
default:
|
|
71
76
|
console.warn(`Unsupported mail driver: ${driver}. Mail sending might fail.`);
|
|
72
|
-
|
|
77
|
+
return null;
|
|
73
78
|
/* eslint-enable indent */
|
|
74
79
|
}
|
|
75
80
|
}
|
|
@@ -115,7 +120,8 @@ class MailerServiceClass {
|
|
|
115
120
|
console.log(`Dry run: Sending mail to ${options.to} with subject ${options.subject}`);
|
|
116
121
|
return;
|
|
117
122
|
}
|
|
118
|
-
|
|
123
|
+
const strategy = this.strategy;
|
|
124
|
+
if (!strategy) {
|
|
119
125
|
console.error('No mailer strategy configured.');
|
|
120
126
|
return;
|
|
121
127
|
}
|
|
@@ -133,7 +139,7 @@ class MailerServiceClass {
|
|
|
133
139
|
html = options.body.includes('<') ? options.body : `<p>${options.body}</p>`;
|
|
134
140
|
}
|
|
135
141
|
try {
|
|
136
|
-
yield
|
|
142
|
+
yield strategy.send({
|
|
137
143
|
to: options.to,
|
|
138
144
|
subject: options.subject,
|
|
139
145
|
html: html,
|
|
@@ -81,16 +81,14 @@ const userSchema = new mongoose_1.Schema({
|
|
|
81
81
|
}, {
|
|
82
82
|
timestamps: true,
|
|
83
83
|
collection: model_names_1.MODELS.user,
|
|
84
|
-
toJSON: {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
},
|
|
93
|
-
},
|
|
84
|
+
// toJSON: {
|
|
85
|
+
// transform(doc, ret) {
|
|
86
|
+
// if (ret.coverImage) ret.coverImage = process.env.BUCKET_HOST + '/' + ret.coverImage;
|
|
87
|
+
// if (ret.profileImage) ret.profileImage = process.env.BUCKET_HOST + '/' + ret.profileImage;
|
|
88
|
+
// if (ret.faceRecognition)
|
|
89
|
+
// ret.faceRecognition = process.env.BUCKET_HOST + '/' + ret.faceRecognition;
|
|
90
|
+
// },
|
|
91
|
+
// },
|
|
94
92
|
})
|
|
95
93
|
.index({ name: 'text' })
|
|
96
94
|
.index({ location: '2dsphere' })
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
private
|
|
3
|
-
private
|
|
4
|
-
private
|
|
5
|
-
|
|
1
|
+
declare class BucketWasabi {
|
|
2
|
+
private _s3;
|
|
3
|
+
private _rekognition;
|
|
4
|
+
private get s3();
|
|
5
|
+
private get rekognition();
|
|
6
|
+
private get bucketName();
|
|
6
7
|
saveBucketFiles(folder: string, ...files: Express.Multer.File[]): Promise<void>;
|
|
7
8
|
private uploadSmallFile;
|
|
8
9
|
removeBucketFiles(...filePaths: string[]): Promise<void>;
|
|
9
10
|
private getContentType;
|
|
11
|
+
getPresignedUrl(fileKey: string): Promise<string>;
|
|
12
|
+
private getImageBytes;
|
|
10
13
|
validateFace(imageKey: string): Promise<{
|
|
11
14
|
isValid: boolean;
|
|
12
15
|
error?: {
|
|
@@ -18,10 +21,6 @@ export declare class Bucket {
|
|
|
18
21
|
private performAntiSpoofingChecks;
|
|
19
22
|
private validateEyesForLiveness;
|
|
20
23
|
private validatePoseForLiveness;
|
|
21
|
-
/**
|
|
22
|
-
* Comprehensive face validation with advanced liveness detection
|
|
23
|
-
* This method provides additional verification by analyzing multiple image properties
|
|
24
|
-
*/
|
|
25
24
|
validateFaceWithAdvancedLiveness(imageKey: string): Promise<{
|
|
26
25
|
isValid: boolean;
|
|
27
26
|
livenessScore: number;
|
|
@@ -36,3 +35,5 @@ export declare class Bucket {
|
|
|
36
35
|
};
|
|
37
36
|
}>;
|
|
38
37
|
}
|
|
38
|
+
export declare const bucketWasabi: BucketWasabi;
|
|
39
|
+
export {};
|
|
@@ -12,29 +12,49 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.
|
|
15
|
+
exports.bucketWasabi = void 0;
|
|
16
16
|
const path_1 = __importDefault(require("path"));
|
|
17
17
|
const aws_sdk_1 = __importDefault(require("aws-sdk"));
|
|
18
|
-
|
|
18
|
+
const redis_connection_1 = require("../middlewares/redis-connection");
|
|
19
|
+
class BucketWasabi {
|
|
19
20
|
constructor() {
|
|
20
|
-
this.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
this._s3 = null;
|
|
22
|
+
this._rekognition = null;
|
|
23
|
+
}
|
|
24
|
+
get s3() {
|
|
25
|
+
if (!this._s3) {
|
|
26
|
+
this._s3 = new aws_sdk_1.default.S3({
|
|
27
|
+
credentials: {
|
|
28
|
+
accessKeyId: process.env.WASABI_ACCESS_KEY,
|
|
29
|
+
secretAccessKey: process.env.WASABI_SECRET_KEY,
|
|
30
|
+
},
|
|
31
|
+
region: process.env.WASABI_REGION || 'us-east-1',
|
|
32
|
+
endpoint: process.env.WASABI_ENDPOINT ||
|
|
33
|
+
`https://s3.${process.env.WASABI_REGION || 'us-east-1'}.wasabisys.com`,
|
|
34
|
+
s3ForcePathStyle: true,
|
|
35
|
+
signatureVersion: 'v4',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return this._s3;
|
|
39
|
+
}
|
|
40
|
+
get rekognition() {
|
|
41
|
+
if (!this._rekognition) {
|
|
42
|
+
this._rekognition = new aws_sdk_1.default.Rekognition({
|
|
43
|
+
accessKeyId: process.env.BUCKET_ACESS_KEY,
|
|
44
|
+
secretAccessKey: process.env.BUCKET_SECRET_KEY,
|
|
45
|
+
region: process.env.BUCKET_REGION || 'us-east-1',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return this._rekognition;
|
|
49
|
+
}
|
|
50
|
+
get bucketName() {
|
|
51
|
+
return process.env.WASABI_BUCKET_NAME;
|
|
31
52
|
}
|
|
32
53
|
saveBucketFiles(folder, ...files) {
|
|
33
54
|
return __awaiter(this, void 0, void 0, function* () {
|
|
34
55
|
const CHUNK_SIZE = 20 * 1024 * 1024;
|
|
35
56
|
const MAX_PARALLEL_FILES = 30;
|
|
36
57
|
const MAX_RETRIES = 3;
|
|
37
|
-
// Process files in parallel batches
|
|
38
58
|
for (let i = 0; i < files.length; i += MAX_PARALLEL_FILES) {
|
|
39
59
|
const fileBatch = files.slice(i, i + MAX_PARALLEL_FILES);
|
|
40
60
|
yield Promise.all(fileBatch.map((file) => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -45,14 +65,12 @@ class Bucket {
|
|
|
45
65
|
}
|
|
46
66
|
let multipartUpload;
|
|
47
67
|
try {
|
|
48
|
-
// Initiate multipart upload
|
|
49
68
|
multipartUpload = yield new Promise((resolve, reject) => {
|
|
50
69
|
this.s3.createMultipartUpload({
|
|
51
70
|
Bucket: this.bucketName,
|
|
52
71
|
Key: `${folder}/${file.filename}`,
|
|
53
72
|
ContentType: contentType,
|
|
54
73
|
ContentDisposition: 'inline',
|
|
55
|
-
ServerSideEncryption: 'AES256',
|
|
56
74
|
}, (err, data) => {
|
|
57
75
|
if (err)
|
|
58
76
|
reject(err);
|
|
@@ -62,13 +80,11 @@ class Bucket {
|
|
|
62
80
|
});
|
|
63
81
|
const uploadId = multipartUpload.UploadId;
|
|
64
82
|
const parts = [];
|
|
65
|
-
// Split buffer into chunks
|
|
66
83
|
const buffer = file.buffer;
|
|
67
84
|
const chunks = [];
|
|
68
85
|
for (let i = 0; i < buffer.length; i += CHUNK_SIZE) {
|
|
69
86
|
chunks.push(buffer.slice(i, i + CHUNK_SIZE));
|
|
70
87
|
}
|
|
71
|
-
// Upload chunks
|
|
72
88
|
const partUploads = chunks.map((chunk, index) => {
|
|
73
89
|
const partNumber = index + 1;
|
|
74
90
|
const uploadChunkWithRetry = (...args_1) => __awaiter(this, [...args_1], void 0, function* (retryCount = 0) {
|
|
@@ -110,7 +126,6 @@ class Bucket {
|
|
|
110
126
|
});
|
|
111
127
|
const completedParts = yield Promise.all(partUploads);
|
|
112
128
|
parts.push(...completedParts.sort((a, b) => { var _a, _b; return ((_a = a.PartNumber) !== null && _a !== void 0 ? _a : 0) - ((_b = b.PartNumber) !== null && _b !== void 0 ? _b : 0); }));
|
|
113
|
-
// Complete multipart upload
|
|
114
129
|
yield new Promise((resolve, reject) => {
|
|
115
130
|
this.s3.completeMultipartUpload({
|
|
116
131
|
Bucket: this.bucketName,
|
|
@@ -150,7 +165,6 @@ class Bucket {
|
|
|
150
165
|
Body: file.buffer,
|
|
151
166
|
ContentDisposition: 'inline',
|
|
152
167
|
ContentType: contentType,
|
|
153
|
-
ServerSideEncryption: 'AES256',
|
|
154
168
|
}, (err, data) => {
|
|
155
169
|
if (err)
|
|
156
170
|
reject(err);
|
|
@@ -188,18 +202,45 @@ class Bucket {
|
|
|
188
202
|
}
|
|
189
203
|
return 'application/octet-stream';
|
|
190
204
|
}
|
|
205
|
+
getPresignedUrl(fileKey) {
|
|
206
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
207
|
+
console.log('[getPresignedUrl] fileKey:', fileKey, '| bucket:', this.bucketName);
|
|
208
|
+
const redis = yield (0, redis_connection_1.getRedisClient)();
|
|
209
|
+
const cacheKey = `wasabi:presigned:${fileKey}`;
|
|
210
|
+
const cached = yield redis.get(cacheKey);
|
|
211
|
+
if (cached)
|
|
212
|
+
return cached;
|
|
213
|
+
const url = this.s3.getSignedUrl('getObject', {
|
|
214
|
+
Bucket: this.bucketName,
|
|
215
|
+
Key: fileKey,
|
|
216
|
+
Expires: 3600,
|
|
217
|
+
});
|
|
218
|
+
console.log('[getPresignedUrl] generated url:', url);
|
|
219
|
+
yield redis.set(cacheKey, url, 'EX', 3000);
|
|
220
|
+
return url;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
// Fetches the image from Wasabi and returns it as bytes for Rekognition
|
|
224
|
+
getImageBytes(imageKey) {
|
|
225
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
226
|
+
const data = yield new Promise((resolve, reject) => {
|
|
227
|
+
this.s3.getObject({ Bucket: this.bucketName, Key: imageKey }, (err, data) => {
|
|
228
|
+
if (err)
|
|
229
|
+
reject(err);
|
|
230
|
+
else
|
|
231
|
+
resolve(data);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
return data.Body;
|
|
235
|
+
});
|
|
236
|
+
}
|
|
191
237
|
validateFace(imageKey) {
|
|
192
238
|
return __awaiter(this, void 0, void 0, function* () {
|
|
193
239
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
194
240
|
try {
|
|
195
|
-
|
|
241
|
+
const imageBytes = yield this.getImageBytes(imageKey);
|
|
196
242
|
const faceParams = {
|
|
197
|
-
Image: {
|
|
198
|
-
S3Object: {
|
|
199
|
-
Bucket: this.bucketName,
|
|
200
|
-
Name: imageKey,
|
|
201
|
-
},
|
|
202
|
-
},
|
|
243
|
+
Image: { Bytes: imageBytes },
|
|
203
244
|
Attributes: ['ALL'],
|
|
204
245
|
};
|
|
205
246
|
const faceResult = yield this.rekognition.detectFaces(faceParams).promise();
|
|
@@ -219,25 +260,22 @@ class Bucket {
|
|
|
219
260
|
};
|
|
220
261
|
}
|
|
221
262
|
const face = faceResult.FaceDetails[0];
|
|
222
|
-
// Enhanced confidence check for liveness
|
|
223
263
|
if (!face.Confidence || face.Confidence < 75) {
|
|
224
264
|
return {
|
|
225
265
|
isValid: false,
|
|
226
266
|
error: {
|
|
227
267
|
en: 'Image quality is not sufficient for verification',
|
|
228
|
-
ar: 'جودة الصورة غير كافية للتحقق'
|
|
268
|
+
ar: 'جودة الصورة غير كافية للتحقق',
|
|
229
269
|
},
|
|
230
270
|
};
|
|
231
271
|
}
|
|
232
|
-
|
|
233
|
-
const livenessCheck = yield this.performLivenessDetection(imageKey);
|
|
272
|
+
const livenessCheck = yield this.performLivenessDetection(imageBytes);
|
|
234
273
|
if (!livenessCheck.isLive) {
|
|
235
274
|
return {
|
|
236
275
|
isValid: false,
|
|
237
276
|
error: livenessCheck.error,
|
|
238
277
|
};
|
|
239
278
|
}
|
|
240
|
-
// Enhanced anti-spoofing checks
|
|
241
279
|
const antiSpoofingCheck = yield this.performAntiSpoofingChecks(face);
|
|
242
280
|
if (!antiSpoofingCheck.isValid) {
|
|
243
281
|
return {
|
|
@@ -245,7 +283,6 @@ class Bucket {
|
|
|
245
283
|
error: antiSpoofingCheck.error,
|
|
246
284
|
};
|
|
247
285
|
}
|
|
248
|
-
// Original validation checks with enhanced thresholds
|
|
249
286
|
if (((_a = face.Sunglasses) === null || _a === void 0 ? void 0 : _a.Value) || ((_b = face.Eyeglasses) === null || _b === void 0 ? void 0 : _b.Value)) {
|
|
250
287
|
return {
|
|
251
288
|
isValid: false,
|
|
@@ -255,7 +292,6 @@ class Bucket {
|
|
|
255
292
|
},
|
|
256
293
|
};
|
|
257
294
|
}
|
|
258
|
-
// Enhanced eye detection for liveness
|
|
259
295
|
const eyesCheck = this.validateEyesForLiveness(face);
|
|
260
296
|
if (!eyesCheck.isValid) {
|
|
261
297
|
return {
|
|
@@ -269,7 +305,6 @@ class Bucket {
|
|
|
269
305
|
error: { en: 'Mouth should be closed', ar: 'يجب أن يكون الفم مغلقاً' },
|
|
270
306
|
};
|
|
271
307
|
}
|
|
272
|
-
// Enhanced pose validation for anti-spoofing
|
|
273
308
|
const poseCheck = this.validatePoseForLiveness(face);
|
|
274
309
|
if (!poseCheck.isValid) {
|
|
275
310
|
return {
|
|
@@ -286,7 +321,6 @@ class Bucket {
|
|
|
286
321
|
},
|
|
287
322
|
};
|
|
288
323
|
}
|
|
289
|
-
// Reject excessive smiling which might indicate a photo
|
|
290
324
|
if (((_j = face.Smile) === null || _j === void 0 ? void 0 : _j.Value) && ((_l = (_k = face.Smile) === null || _k === void 0 ? void 0 : _k.Confidence) !== null && _l !== void 0 ? _l : 0) > 80) {
|
|
291
325
|
return {
|
|
292
326
|
isValid: false,
|
|
@@ -307,21 +341,12 @@ class Bucket {
|
|
|
307
341
|
}
|
|
308
342
|
});
|
|
309
343
|
}
|
|
310
|
-
performLivenessDetection(
|
|
344
|
+
performLivenessDetection(imageBytes) {
|
|
311
345
|
return __awaiter(this, void 0, void 0, function* () {
|
|
312
346
|
try {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
S3Object: {
|
|
317
|
-
Bucket: this.bucketName,
|
|
318
|
-
Name: imageKey,
|
|
319
|
-
},
|
|
320
|
-
},
|
|
321
|
-
};
|
|
322
|
-
const textResult = yield this.rekognition.detectText(textParams).promise();
|
|
323
|
-
// If significant text is detected around the face area, it might be a printed photo
|
|
324
|
-
// Increased threshold to reduce false positives
|
|
347
|
+
const textResult = yield this.rekognition
|
|
348
|
+
.detectText({ Image: { Bytes: imageBytes } })
|
|
349
|
+
.promise();
|
|
325
350
|
if (textResult.TextDetections && textResult.TextDetections.length > 8) {
|
|
326
351
|
return {
|
|
327
352
|
isLive: false,
|
|
@@ -331,23 +356,13 @@ class Bucket {
|
|
|
331
356
|
},
|
|
332
357
|
};
|
|
333
358
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
S3Object: {
|
|
338
|
-
Bucket: this.bucketName,
|
|
339
|
-
Name: imageKey,
|
|
340
|
-
},
|
|
341
|
-
},
|
|
342
|
-
MaxLabels: 20,
|
|
343
|
-
MinConfidence: 70,
|
|
344
|
-
};
|
|
345
|
-
const labelResult = yield this.rekognition.detectLabels(labelParams).promise();
|
|
359
|
+
const labelResult = yield this.rekognition
|
|
360
|
+
.detectLabels({ Image: { Bytes: imageBytes }, MaxLabels: 20, MinConfidence: 70 })
|
|
361
|
+
.promise();
|
|
346
362
|
if (labelResult.Labels) {
|
|
347
|
-
// Reduced list to only obvious screen/printed content and increased confidence threshold
|
|
348
363
|
const suspiciousLabels = ['Screen', 'Monitor', 'Display', 'Computer Screen', 'Television'];
|
|
349
364
|
for (const label of labelResult.Labels) {
|
|
350
|
-
if (suspiciousLabels.some(suspicious => {
|
|
365
|
+
if (suspiciousLabels.some((suspicious) => {
|
|
351
366
|
var _a, _b;
|
|
352
367
|
return ((_a = label.Name) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes(suspicious.toLowerCase())) &&
|
|
353
368
|
((_b = label.Confidence) !== null && _b !== void 0 ? _b : 0) > 85;
|
|
@@ -366,8 +381,6 @@ class Bucket {
|
|
|
366
381
|
}
|
|
367
382
|
catch (error) {
|
|
368
383
|
console.error('Error in liveness detection:', error);
|
|
369
|
-
// Don't fail the entire validation for liveness detection errors
|
|
370
|
-
// This prevents AWS service issues from blocking legitimate users
|
|
371
384
|
return { isLive: true };
|
|
372
385
|
}
|
|
373
386
|
});
|
|
@@ -375,7 +388,6 @@ class Bucket {
|
|
|
375
388
|
performAntiSpoofingChecks(face) {
|
|
376
389
|
return __awaiter(this, void 0, void 0, function* () {
|
|
377
390
|
try {
|
|
378
|
-
// Check for overly perfect lighting (common in printed photos) - relaxed threshold
|
|
379
391
|
const qualityMetrics = face.Quality;
|
|
380
392
|
if ((qualityMetrics === null || qualityMetrics === void 0 ? void 0 : qualityMetrics.Brightness) && qualityMetrics.Brightness > 98) {
|
|
381
393
|
return {
|
|
@@ -386,8 +398,8 @@ class Bucket {
|
|
|
386
398
|
},
|
|
387
399
|
};
|
|
388
400
|
}
|
|
389
|
-
|
|
390
|
-
|
|
401
|
+
if ((qualityMetrics === null || qualityMetrics === void 0 ? void 0 : qualityMetrics.Sharpness) &&
|
|
402
|
+
(qualityMetrics.Sharpness < 10 || qualityMetrics.Sharpness > 98)) {
|
|
391
403
|
return {
|
|
392
404
|
isValid: false,
|
|
393
405
|
error: {
|
|
@@ -396,13 +408,14 @@ class Bucket {
|
|
|
396
408
|
},
|
|
397
409
|
};
|
|
398
410
|
}
|
|
399
|
-
// Enhanced landmark analysis for 3D face detection
|
|
400
411
|
if (face.Landmarks && face.Landmarks.length > 0) {
|
|
401
412
|
const landmarks = face.Landmarks;
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
l.Type === 'leftEyeLeft' ||
|
|
405
|
-
l.Type === '
|
|
413
|
+
const eyeLandmarks = landmarks.filter((l) => l.Type === 'eyeLeft' ||
|
|
414
|
+
l.Type === 'eyeRight' ||
|
|
415
|
+
l.Type === 'leftEyeLeft' ||
|
|
416
|
+
l.Type === 'leftEyeRight' ||
|
|
417
|
+
l.Type === 'rightEyeLeft' ||
|
|
418
|
+
l.Type === 'rightEyeRight');
|
|
406
419
|
if (eyeLandmarks.length < 4) {
|
|
407
420
|
return {
|
|
408
421
|
isValid: false,
|
|
@@ -412,24 +425,17 @@ class Bucket {
|
|
|
412
425
|
},
|
|
413
426
|
};
|
|
414
427
|
}
|
|
415
|
-
// Check for unnatural symmetry (printed photos often have perfect symmetry)
|
|
416
|
-
const noseLandmarks = landmarks.filter(l => l.Type === 'nose' || l.Type === 'noseLeft' || l.Type === 'noseRight');
|
|
417
|
-
if (noseLandmarks.length > 0) {
|
|
418
|
-
// Additional geometric checks could be implemented here
|
|
419
|
-
}
|
|
420
428
|
}
|
|
421
429
|
return { isValid: true };
|
|
422
430
|
}
|
|
423
431
|
catch (error) {
|
|
424
432
|
console.error('Error in anti-spoofing checks:', error);
|
|
425
|
-
// Don't fail validation for anti-spoofing errors to prevent service issues
|
|
426
433
|
return { isValid: true };
|
|
427
434
|
}
|
|
428
435
|
});
|
|
429
436
|
}
|
|
430
437
|
validateEyesForLiveness(face) {
|
|
431
438
|
var _a;
|
|
432
|
-
// Enhanced eye validation for liveness detection - more lenient
|
|
433
439
|
const eyesOpen = face.EyesOpen;
|
|
434
440
|
if (!(eyesOpen === null || eyesOpen === void 0 ? void 0 : eyesOpen.Value) || ((_a = eyesOpen.Confidence) !== null && _a !== void 0 ? _a : 0) < 70) {
|
|
435
441
|
return {
|
|
@@ -440,9 +446,8 @@ class Bucket {
|
|
|
440
446
|
},
|
|
441
447
|
};
|
|
442
448
|
}
|
|
443
|
-
// Check for natural eye appearance (avoid glass reflections, red-eye, etc.)
|
|
444
449
|
if (face.Landmarks) {
|
|
445
|
-
const eyeLandmarks = face.Landmarks.filter(l => { var _a, _b; return ((_a = l.Type) === null || _a === void 0 ? void 0 : _a.includes('eye')) || ((_b = l.Type) === null || _b === void 0 ? void 0 : _b.includes('Eye')); });
|
|
450
|
+
const eyeLandmarks = face.Landmarks.filter((l) => { var _a, _b; return ((_a = l.Type) === null || _a === void 0 ? void 0 : _a.includes('eye')) || ((_b = l.Type) === null || _b === void 0 ? void 0 : _b.includes('Eye')); });
|
|
446
451
|
if (eyeLandmarks.length < 6) {
|
|
447
452
|
return {
|
|
448
453
|
isValid: false,
|
|
@@ -457,7 +462,7 @@ class Bucket {
|
|
|
457
462
|
}
|
|
458
463
|
validatePoseForLiveness(face) {
|
|
459
464
|
const pose = face.Pose;
|
|
460
|
-
const poseThreshold = 12;
|
|
465
|
+
const poseThreshold = 12;
|
|
461
466
|
if (Math.abs((pose === null || pose === void 0 ? void 0 : pose.Pitch) || 0) > poseThreshold ||
|
|
462
467
|
Math.abs((pose === null || pose === void 0 ? void 0 : pose.Roll) || 0) > poseThreshold ||
|
|
463
468
|
Math.abs((pose === null || pose === void 0 ? void 0 : pose.Yaw) || 0) > poseThreshold) {
|
|
@@ -469,14 +474,8 @@ class Bucket {
|
|
|
469
474
|
},
|
|
470
475
|
};
|
|
471
476
|
}
|
|
472
|
-
// Additional check for unnatural pose stability (printed photos have perfect stability)
|
|
473
|
-
// This would require multiple frames in a video-based solution, but we can check for other indicators
|
|
474
477
|
return { isValid: true };
|
|
475
478
|
}
|
|
476
|
-
/**
|
|
477
|
-
* Comprehensive face validation with advanced liveness detection
|
|
478
|
-
* This method provides additional verification by analyzing multiple image properties
|
|
479
|
-
*/
|
|
480
479
|
validateFaceWithAdvancedLiveness(imageKey) {
|
|
481
480
|
return __awaiter(this, void 0, void 0, function* () {
|
|
482
481
|
var _a, _b, _c;
|
|
@@ -489,34 +488,18 @@ class Bucket {
|
|
|
489
488
|
error: baseValidation.error,
|
|
490
489
|
};
|
|
491
490
|
}
|
|
492
|
-
// Calculate liveness score based on multiple factors
|
|
493
491
|
let livenessScore = 100;
|
|
494
492
|
const livenessIndicators = [];
|
|
495
493
|
const spoofingRisks = [];
|
|
496
|
-
|
|
497
|
-
const
|
|
498
|
-
Image: {
|
|
499
|
-
|
|
500
|
-
Bucket: this.bucketName,
|
|
501
|
-
Name: imageKey,
|
|
502
|
-
},
|
|
503
|
-
},
|
|
504
|
-
Attributes: ['ALL'],
|
|
505
|
-
};
|
|
506
|
-
const faceResult = yield this.rekognition.detectFaces(faceParams).promise();
|
|
494
|
+
const imageBytes = yield this.getImageBytes(imageKey);
|
|
495
|
+
const faceResult = yield this.rekognition
|
|
496
|
+
.detectFaces({ Image: { Bytes: imageBytes }, Attributes: ['ALL'] })
|
|
497
|
+
.promise();
|
|
507
498
|
const face = faceResult.FaceDetails[0];
|
|
508
|
-
// Analyze image metadata for camera vs screenshot indicators
|
|
509
499
|
try {
|
|
510
|
-
const
|
|
511
|
-
Image: {
|
|
512
|
-
|
|
513
|
-
Bucket: this.bucketName,
|
|
514
|
-
Name: imageKey,
|
|
515
|
-
},
|
|
516
|
-
},
|
|
517
|
-
};
|
|
518
|
-
const moderationResult = yield this.rekognition.detectModerationLabels(moderationParams).promise();
|
|
519
|
-
// Check for any labels that might indicate artificial content
|
|
500
|
+
const moderationResult = yield this.rekognition
|
|
501
|
+
.detectModerationLabels({ Image: { Bytes: imageBytes } })
|
|
502
|
+
.promise();
|
|
520
503
|
if (moderationResult.ModerationLabels && moderationResult.ModerationLabels.length > 0) {
|
|
521
504
|
for (const label of moderationResult.ModerationLabels) {
|
|
522
505
|
if (((_a = label.Name) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes('graphic')) && ((_b = label.Confidence) !== null && _b !== void 0 ? _b : 0) > 50) {
|
|
@@ -527,10 +510,8 @@ class Bucket {
|
|
|
527
510
|
}
|
|
528
511
|
}
|
|
529
512
|
catch (error) {
|
|
530
|
-
// Moderation check failed, but don't fail the entire validation
|
|
531
513
|
console.warn('Moderation check failed:', error);
|
|
532
514
|
}
|
|
533
|
-
// Quality metrics analysis
|
|
534
515
|
const quality = face.Quality;
|
|
535
516
|
if (quality) {
|
|
536
517
|
if (quality.Brightness && (quality.Brightness < 30 || quality.Brightness > 90)) {
|
|
@@ -548,7 +529,6 @@ class Bucket {
|
|
|
548
529
|
spoofingRisks.push('Suspicious image sharpness');
|
|
549
530
|
}
|
|
550
531
|
}
|
|
551
|
-
// Face feature confidence analysis
|
|
552
532
|
if (face.Confidence && face.Confidence > 95) {
|
|
553
533
|
livenessIndicators.push('High face detection confidence');
|
|
554
534
|
}
|
|
@@ -556,11 +536,9 @@ class Bucket {
|
|
|
556
536
|
livenessScore -= 20;
|
|
557
537
|
spoofingRisks.push('Low face detection confidence');
|
|
558
538
|
}
|
|
559
|
-
// Eye analysis for natural appearance
|
|
560
539
|
if (((_c = face.EyesOpen) === null || _c === void 0 ? void 0 : _c.Confidence) && face.EyesOpen.Confidence > 95) {
|
|
561
540
|
livenessIndicators.push('Natural eye appearance');
|
|
562
541
|
}
|
|
563
|
-
// Pose analysis for natural positioning
|
|
564
542
|
const pose = face.Pose;
|
|
565
543
|
if (pose) {
|
|
566
544
|
const totalPoseDeviation = Math.abs(pose.Pitch || 0) + Math.abs(pose.Roll || 0) + Math.abs(pose.Yaw || 0);
|
|
@@ -572,7 +550,6 @@ class Bucket {
|
|
|
572
550
|
spoofingRisks.push('Unnaturally perfect pose alignment');
|
|
573
551
|
}
|
|
574
552
|
}
|
|
575
|
-
// Landmark analysis for 3D facial structure
|
|
576
553
|
if (face.Landmarks && face.Landmarks.length >= 15) {
|
|
577
554
|
livenessIndicators.push('Comprehensive facial landmarks detected');
|
|
578
555
|
}
|
|
@@ -580,15 +557,16 @@ class Bucket {
|
|
|
580
557
|
livenessScore -= 10;
|
|
581
558
|
spoofingRisks.push('Insufficient facial landmarks');
|
|
582
559
|
}
|
|
583
|
-
// Final liveness assessment
|
|
584
560
|
const isLive = livenessScore >= 70;
|
|
585
561
|
return {
|
|
586
562
|
isValid: isLive,
|
|
587
563
|
livenessScore,
|
|
588
|
-
error: isLive
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
564
|
+
error: isLive
|
|
565
|
+
? undefined
|
|
566
|
+
: {
|
|
567
|
+
en: `Liveness verification failed (Score: ${livenessScore}/100). Please use a live camera capture`,
|
|
568
|
+
ar: `فشل التحقق من الحيوية (النتيجة: ${livenessScore}/100). يرجى استخدام التقاط مباشر من الكاميرا`,
|
|
569
|
+
},
|
|
592
570
|
details: {
|
|
593
571
|
faceConfidence: face.Confidence || 0,
|
|
594
572
|
livenessIndicators,
|
|
@@ -607,7 +585,6 @@ class Bucket {
|
|
|
607
585
|
});
|
|
608
586
|
}
|
|
609
587
|
}
|
|
610
|
-
exports.Bucket = Bucket;
|
|
611
588
|
const MIME_TYPES = {
|
|
612
589
|
video: {
|
|
613
590
|
'.mp4': 'video/mp4',
|
|
@@ -642,3 +619,4 @@ const MIME_TYPES = {
|
|
|
642
619
|
'.pdf': 'application/pdf',
|
|
643
620
|
},
|
|
644
621
|
};
|
|
622
|
+
exports.bucketWasabi = new BucketWasabi();
|