docs: add quixzoom-auth-core product to AAMOS
- Product documentation in docs/products/ - Updated MEMORY.md with product info - quiXzoom Auth Core as AAMOS Identity product
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* POST /v1/score — Riskpoängsättning
|
||||
* Beräknar riskpoäng baserat på bildanalys + metadata
|
||||
*/
|
||||
import { Router } from 'express';
|
||||
import sharp from 'sharp';
|
||||
import { fetchImage, hashInput, saveResult, genReqId, requireAuth } from './utils.mjs';
|
||||
|
||||
const router = Router();
|
||||
|
||||
async function analyzeImageRisk(buffer) {
|
||||
const { data, info } = await sharp(buffer).resize(128, 128).raw().toBuffer({ resolveWithObject: true });
|
||||
const w = info.width, h = info.height;
|
||||
|
||||
// Analyze image quality metrics
|
||||
let totalBrightness = 0, totalContrast = 0, edgeCount = 0;
|
||||
const brightnessHistogram = new Array(256).fill(0);
|
||||
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const i = (y * w + x) * 3;
|
||||
const gray = (data[i] + data[i+1] + data[i+2]) / 3;
|
||||
totalBrightness += gray;
|
||||
brightnessHistogram[Math.round(gray)]++;
|
||||
|
||||
if (x < w - 1) {
|
||||
const diff = Math.abs(gray - ((data[i+3] + data[i+4] + data[i+5]) / 3));
|
||||
totalContrast += diff;
|
||||
if (diff > 30) edgeCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pixelCount = w * h;
|
||||
const avgBrightness = totalBrightness / pixelCount;
|
||||
const avgContrast = totalContrast / pixelCount;
|
||||
const edgeRatio = edgeCount / pixelCount;
|
||||
|
||||
// Calculate entropy (measure of randomness/complexity)
|
||||
let entropy = 0;
|
||||
for (let i = 0; i < 256; i++) {
|
||||
const p = brightnessHistogram[i] / pixelCount;
|
||||
if (p > 0) entropy -= p * Math.log2(p);
|
||||
}
|
||||
|
||||
return {
|
||||
brightness: Math.round(avgBrightness),
|
||||
contrast: Math.round(avgContrast),
|
||||
edge_ratio: parseFloat(edgeRatio.toFixed(4)),
|
||||
entropy: parseFloat(entropy.toFixed(4)),
|
||||
dark_ratio: parseFloat((brightnessHistogram.slice(0, 50).reduce((a,b)=>a+b,0) / pixelCount).toFixed(4)),
|
||||
bright_ratio: parseFloat((brightnessHistogram.slice(200).reduce((a,b)=>a+b,0) / pixelCount).toFixed(4)),
|
||||
};
|
||||
}
|
||||
|
||||
function calculateRiskScore(analysis, metadata = {}) {
|
||||
let score = 0.3; // Base risk
|
||||
const factors = [];
|
||||
|
||||
// Image quality risks
|
||||
if (analysis.brightness < 30) { score += 0.15; factors.push({ type: 'image', reason: 'Very dark image', contribution: 0.15 }); }
|
||||
if (analysis.brightness > 240) { score += 0.1; factors.push({ type: 'image', reason: 'Overexposed image', contribution: 0.1 }); }
|
||||
if (analysis.contrast < 5) { score += 0.2; factors.push({ type: 'image', reason: 'Low contrast — possible synthetic image', contribution: 0.2 }); }
|
||||
if (analysis.entropy < 3) { score += 0.15; factors.push({ type: 'image', reason: 'Low entropy — possible compression artifact or synthetic', contribution: 0.15 }); }
|
||||
if (analysis.edge_ratio < 0.02) { score += 0.1; factors.push({ type: 'image', reason: 'Few edges — possibly blurred or artificial', contribution: 0.1 }); }
|
||||
|
||||
// Metadata risks
|
||||
if (metadata.source === 'base64' && !metadata.filename) { score += 0.05; factors.push({ type: 'metadata', reason: 'No filename metadata', contribution: 0.05 }); }
|
||||
if (metadata.user_agent && metadata.user_agent.includes('bot')) { score += 0.1; factors.push({ type: 'metadata', reason: 'Bot user agent', contribution: 0.1 }); }
|
||||
|
||||
// Time-based risks
|
||||
const hour = new Date().getUTCHours();
|
||||
if (hour < 5 || hour > 22) { score += 0.05; factors.push({ type: 'time', reason: 'Unusual request hour', contribution: 0.05 }); }
|
||||
|
||||
return {
|
||||
score: parseFloat(Math.min(1.0, score).toFixed(4)),
|
||||
factors: factors.slice(0, 5),
|
||||
analysis,
|
||||
};
|
||||
}
|
||||
|
||||
router.post('/', requireAuth, async (req, res) => {
|
||||
const requestId = genReqId();
|
||||
const start = Date.now();
|
||||
try {
|
||||
const { image_url, image_base64, metadata = {} } = req.body || {};
|
||||
const img = await fetchImage({ image_url, image_base64 });
|
||||
const inputHash = hashInput(img.buffer);
|
||||
|
||||
const analysis = await analyzeImageRisk(img.buffer);
|
||||
const risk = calculateRiskScore(analysis, { ...metadata, source: img.source });
|
||||
|
||||
const result = {
|
||||
ok: true,
|
||||
endpoint: 'score',
|
||||
request_id: requestId,
|
||||
risk_score: risk.score,
|
||||
risk_level: risk.score > 0.7 ? 'high' : risk.score > 0.4 ? 'medium' : 'low',
|
||||
confidence: parseFloat((1 - risk.score * 0.3).toFixed(4)),
|
||||
factors: risk.factors,
|
||||
image_analysis: risk.analysis,
|
||||
recommendations: risk.score > 0.7
|
||||
? ['Require additional verification', 'Flag for manual review', 'Check device fingerprint']
|
||||
: risk.score > 0.4
|
||||
? ['Standard verification recommended', 'Monitor for anomalies']
|
||||
: ['Low risk — standard processing'],
|
||||
inference_time_ms: Date.now() - start,
|
||||
};
|
||||
|
||||
await saveResult('score', requestId, inputHash, result, result.confidence, { source: img.source, metadata });
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
console.error('[score]', e);
|
||||
res.status(500).json({ ok: false, error: e.message, request_id: requestId });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user