meta: initial work

doesn't fully work yet
This commit is contained in:
eva
2026-02-11 18:28:34 +00:00
commit 735e7ef53f
25 changed files with 4990 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Cloudflare Types
/worker-configuration.d.ts
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+9
View File
@@ -0,0 +1,9 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
# Miscellaneous
/static/
+16
View File
@@ -0,0 +1,16 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
],
"tailwindStylesheet": "./src/routes/layout.css"
}
+5
View File
@@ -0,0 +1,5 @@
{
"files.associations": {
"*.css": "tailwindcss"
}
}
+3
View File
@@ -0,0 +1,3 @@
# k-id age verifier
more information [on the website](https://age-verifier.eva.ac)
+39
View File
@@ -0,0 +1,39 @@
import prettier from 'eslint-config-prettier';
import path from 'node:path';
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import { defineConfig } from 'eslint/config';
import globals from 'globals';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
const gitignorePath = path.resolve(import.meta.dirname, '.gitignore');
export default defineConfig(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
prettier,
...svelte.configs.prettier,
{
languageOptions: { globals: { ...globals.browser, ...globals.node } },
rules: {
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
'no-undef': 'off'
}
},
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: ['.svelte'],
parser: ts.parser,
svelteConfig
}
}
}
);
+40
View File
@@ -0,0 +1,40 @@
{
"name": "k-id-age-verifier",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "wrangler dev .svelte-kit/cloudflare/_worker.js --port 4173",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"gen": "wrangler types"
},
"devDependencies": {
"@eslint/compat": "^2.0.2",
"@eslint/js": "^9.39.2",
"@sveltejs/adapter-cloudflare": "^7.2.6",
"@sveltejs/kit": "^2.50.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/vite": "^4.1.18",
"@types/node": "^24",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.14.0",
"globals": "^17.3.0",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.4.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"svelte": "^5.49.2",
"svelte-check": "^4.3.6",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"typescript-eslint": "^8.54.0",
"vite": "^7.3.1",
"wrangler": "^4.63.0"
}
}
+3026
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- esbuild
+1
View File
@@ -0,0 +1 @@
{}
+747
View File
@@ -0,0 +1,747 @@
import { Buffer } from 'node:buffer';
const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
const randomFloat = (min: number, max: number, decimals = 15) =>
parseFloat((Math.random() * (max - min) + min).toFixed(decimals));
const randomChoice = <T>(arr: T[]): T => arr[Math.floor(Math.random() * arr.length)];
const BASE_URL = 'https://eu-west-1.faceassure.com';
function generateUserAgent() {
const agents = [
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1',
'Mozilla/5.0 (Android 13; Mobile; rv:109.0) Gecko/109.0 Firefox/117.0',
'Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36'
];
return agents[Math.floor(Math.random() * agents.length)];
}
function parseUserAgent(userAgent: string) {
const isIOS = /iPhone|iPad/.test(userAgent);
const isAndroid = /Android/.test(userAgent);
const isSafari = /Safari/.test(userAgent) && !/Chrome/.test(userAgent);
const isChrome = /Chrome/.test(userAgent);
const isFirefox = /Firefox/.test(userAgent);
return {
browser: {
name: isSafari ? 'Safari' : isChrome ? 'Chrome' : isFirefox ? 'Firefox' : 'Safari',
version: isIOS ? '17.0' : '117.0'
},
device: {
type: 'mobile',
vendor: isIOS ? 'Apple' : 'Samsung',
model: isIOS ? 'iPhone' : 'Galaxy'
},
os: {
name: isIOS ? 'iOS' : isAndroid ? 'Android' : 'iOS',
version: isIOS ? '17.0' : '13'
},
engine: { name: isSafari || isIOS ? 'WebKit' : 'Blink' },
cpu: { architecture: '64' }
};
}
function getRandomLocation() {
const locations = [
{
country: 'United States',
state: 'California',
timezone: 'America/Los_Angeles',
lang: 'en-US,en;q=0.9'
},
{
country: 'United States',
state: 'New York',
timezone: 'America/New_York',
lang: 'en-US,en;q=0.9'
},
{
country: 'Canada',
state: 'Ontario',
timezone: 'America/Toronto',
lang: 'en-CA,en;q=0.9,fr;q=0.8'
},
{
country: 'Australia',
state: 'New South Wales',
timezone: 'Australia/Sydney',
lang: 'en-AU,en;q=0.9'
},
{
country: 'Germany',
state: 'Berlin',
timezone: 'Europe/Berlin',
lang: 'de-DE,de;q=0.9,en;q=0.8'
},
{
country: 'France',
state: 'Île-de-France',
timezone: 'Europe/Paris',
lang: 'fr-FR,fr;q=0.9,en;q=0.8'
},
{
country: 'Netherlands',
state: 'North Holland',
timezone: 'Europe/Amsterdam',
lang: 'nl-NL,nl;q=0.9,en;q=0.8'
},
{
country: 'Sweden',
state: 'Stockholm',
timezone: 'Europe/Stockholm',
lang: 'sv-SE,sv;q=0.9,en;q=0.8'
},
{
country: 'Norway',
state: 'Oslo',
timezone: 'Europe/Oslo',
lang: 'nb-NO,nb;q=0.9,en;q=0.8'
}
];
return locations[Math.floor(Math.random() * locations.length)];
}
function generateMediaMetadata() {
const randomHex = () =>
Array.from({ length: 32 }, () =>
Math.floor(Math.random() * 16)
.toString(16)
.toUpperCase()
).join('');
const specs = [
{
width: 4032,
height: 3024,
frameRate: 60,
zoom: 10,
aspectRatio: 4032 / 3024
},
{
width: 3840,
height: 2160,
frameRate: 60,
zoom: 8,
aspectRatio: 3840 / 2160
},
{
width: 1920,
height: 1080,
frameRate: 120,
zoom: 5,
aspectRatio: 1920 / 1080
},
{
width: 2560,
height: 1440,
frameRate: 60,
zoom: 6,
aspectRatio: 2560 / 1440
}
];
const spec = randomChoice(specs);
const deviceId = randomHex();
return [
{
mediaKind: 'audioinput',
mediaLabel: randomChoice(['', 'Built-in Microphone', 'Default']),
mediaId: randomHex(),
mediaCapabilities: {}
},
{
mediaKind: 'videoinput',
mediaLabel: 'Front Camera',
mediaId: deviceId,
mediaCapabilities: {
aspectRatio: {
max: spec.aspectRatio,
min: randomFloat(0.0003, 0.001, 15)
},
backgroundBlur: [false],
deviceId: deviceId,
facingMode: ['user'],
focusDistance: { min: randomFloat(0.1, 0.3) },
frameRate: { max: spec.frameRate, min: 1 },
groupId: randomHex(),
height: { max: spec.height, min: 1 },
powerEfficient: [false, true],
whiteBalanceMode: randomChoice([
['manual', 'continuous'],
['auto', 'manual'],
['continuous']
]),
width: { max: spec.width, min: 1 },
zoom: { max: spec.zoom, min: 1 }
}
}
];
}
const AMAP_MAP: Record<number, [number, number]> = {
0: [0, 2],
1: [2, 4],
2: [4, 8],
3: [8, 13],
4: [13, 18],
5: [18, 21],
6: [21, 25],
7: [25, 28],
8: [28, 32],
9: [32, 36],
10: [36, 40],
11: [40, 45],
12: [45, 50],
13: [50, 60],
14: [60, 70],
15: [70, 120]
};
function amap(e: number) {
const n = AMAP_MAP[~~e];
const r = e % 1;
return n[0] + r * (n[1] - n[0]);
}
function removeOutliersWithZscore(arr: number[]) {
const r =
arr.reduce(function (e, t) {
return e + t;
}, 0) / arr.length;
const a =
arr.reduce(function (e, t) {
return e + Math.pow(t - r, 2);
}, 0) / arr.length;
const s = Math.sqrt(a);
return arr.filter(function (e) {
return Math.abs((e - r) / s) <= 1;
});
}
async function encryptPayload(nonce: string, payload: any) {
const getKey = async (nonce: string, timestamp: string, transactionId: string) => {
const data = nonce + timestamp + transactionId;
const dataEncoded = new TextEncoder().encode(data);
const key = await crypto.subtle.importKey(
'raw',
dataEncoded,
{
name: 'HKDF'
},
false,
['deriveBits']
);
const derived = await crypto.subtle.deriveBits(
{
name: 'HKDF',
hash: 'SHA-256',
salt: new Uint8Array(0),
info: new TextEncoder().encode('payload-encryption')
},
key,
32 * 8
);
return await crypto.subtle.importKey(
'raw',
derived,
{
name: 'AES-GCM'
},
false,
['encrypt']
);
};
const timestamp = new Date().toISOString();
const key = await getKey(nonce, timestamp, payload.transaction_id);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encryptedBuffer = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
new TextEncoder().encode(JSON.stringify(payload))
);
const rawBuffer = Buffer.from(encryptedBuffer);
const encryptedPayloadBuf = rawBuffer.subarray(0, rawBuffer.length - 16);
const authTagBuf = rawBuffer.subarray(rawBuffer.length - 16);
return {
encrypted_payload: encryptedPayloadBuf.toString('base64'),
iv: Buffer.from(iv).toString('base64'),
auth_tag: authTagBuf.toString('base64'),
timestamp
};
}
const qrCodeUrl = Deno.args[0];
async function verify(qrCodeUrlStr: string) {
const userAgent = generateUserAgent();
const parsedUserAgent = parseUserAgent(userAgent);
const location = getRandomLocation();
const mediaMetadata = generateMediaMetadata();
const commonHeaders = {
'User-Agent': userAgent,
accept: '*/*',
'accept-language': location.lang,
'access-control-allow-origin': '*',
priority: 'u=1, i',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'cross-site'
};
const qrCodeUrl = new URL(qrCodeUrlStr);
const shortlinkId = qrCodeUrl.searchParams.get('sl');
if (!shortlinkId) throw `shortlink id not found in qr code url`;
const res = await fetch(`${BASE_URL}/shortlinks/${shortlinkId}`, {
headers: commonHeaders
});
if (!res.ok)
`failed to get shortlink (status=${res.status}, body=${JSON.stringify(await res.text())})`;
const data = await res.json();
const originalUrl = new URL(data.Item.original_url.S.replace('#', ''));
const token = originalUrl.searchParams.get('token');
if (!token) throw `token not found in original url`;
const parts = token.split('.');
if (parts.length !== 3) throw `token is an invalid jwt `;
const jwtPayload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
const sessionRes = await fetch(`${BASE_URL}/age-services/d-privately-age-services`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...commonHeaders
},
body: JSON.stringify({
request_type: 'generate_new_session',
transaction_id: jwtPayload.jti,
api_key: null,
api_secret: null,
token,
longURL: null,
userAgent: userAgent
})
});
if (!sessionRes.ok)
throw `failed to generate new session (status=${sessionRes.status}, body=${JSON.stringify(await sessionRes.text())})`;
const sessionData = await sessionRes.json();
const generateBoundingBox = () => {
const topLeft = [randomFloat(140, 160), randomFloat(250, 270)];
const width = randomFloat(170, 190);
const height = randomFloat(220, 240);
return {
topLeft,
bottomRight: [topLeft[0] + width, topLeft[1] + height],
width,
height
};
};
const generateTimeline = (maxTime: number) => {
const entries = [];
for (let i = 0; i < randomInt(2, 5); i++) {
const start = randomInt(1, maxTime - 100);
const end = start + randomInt(50, 500);
if (end < maxTime) entries.push([start, end]);
}
return entries;
};
const generateStateTimelines = (completionTime: number) => {
const states = [
'TIME_UNTIL_CLICK_START',
'GET_READY',
'NO_FACE',
'LOOK_STRAIGHT',
'TURN_LEFT',
'CENTRE_FACE',
'KEEP_YOUR_MOUTH_OPEN',
'CLOSE_YOUR_MOUTH',
'SLOWLY_COME_CLOSER_TO_THE_CAMERA',
'SLOWLY_DISTANCE_YOURSELF_FROM_THE_CAMERA',
'TOO_DARK'
];
const timelines: Record<string, number[][]> = {};
states.forEach((state) => (timelines[state] = generateTimeline(completionTime)));
return timelines;
};
const baseAge = randomFloat(25.2, 26.0);
const minAge = baseAge - randomFloat(0.1, 0.5);
const maxAge = baseAge + randomFloat(0.1, 0.5);
const averageAge = (minAge + maxAge) / 2;
const currentTime = Date.now() / 1000;
const completionTime = randomInt(8000, 15000);
const raws = Array.from({ length: 10 }, () => randomFloat(6.005, 7.007));
const primaryOutputs = removeOutliersWithZscore(raws.map((r) => amap(r)));
const outputs = removeOutliersWithZscore(primaryOutputs);
let payload = {
request_type: 'complete_transaction',
transaction_id: sessionData.transaction_id,
api_key: sessionData.session_id,
api_secret: sessionData.session_password,
remote_pld: {},
browser_response_data: {
age: 'yes',
age_confidence: 1,
genuineness: Array.from({ length: 5 }, () => randomFloat(0.4, 0.98)),
product: 'age',
modality: 'image',
unverifiedPayload: {
iss: 'https://api.privately.swiss',
sub: '1024',
aud: 'https://api.k-id.com',
exp: jwtPayload.exp,
nbf: jwtPayload.nbf,
iat: jwtPayload.iat,
jti: jwtPayload.jti,
age: jwtPayload.age,
liv: true,
rlt: {
minAge,
maxAge,
score: 0,
gate: 16
},
rsn: 'complete_transaction',
rtf: 'interval',
rtb: 'callback',
vid: jwtPayload.vid,
ver: 'v1.10.22',
ufi: []
},
ageCheckSession: '-' + randomInt(1000000000, 9999999999),
miscellaneous: {
recordedOpennessStreak: Array.from({ length: 5 }, () => randomFloat(0.1, 0.8, 17)),
recordedSpeeds: Array.from({ length: 5 }, () => randomFloat(0.2, 1.5, 17)),
recordedIntervals: Array.from({ length: 5 }, () => randomFloat(0.1, 0.2, 3)),
failedOpennessReadings: [],
failedOpennessSpeeds: [],
failedOpennessIntervals: [],
numberOfGestureRetries: 1,
antiSpoofConfidences: [],
fp_scores: [],
laplacian_blur_scores: Array.from({ length: 300 }, () => randomFloat(10, 300, 15)),
laplacian_min_score: randomFloat(10, 50),
laplacian_max_score: randomFloat(300, 350),
laplacian_avg_score: randomFloat(50, 100),
glare_ratios: Array.from({ length: 300 }, () => 0),
allScreenDetectionDetails: {
beforeClickingStart: {
screenDetectionConfidence: [],
screenFaceOverlap: [],
screenBoundingBoxes: [],
alternativeScore: []
},
positioning: {
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
screenFaceOverlap: [0, 0],
screenBoundingBoxes: [[], []],
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
},
liveness: {
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
screenFaceOverlap: [0, 0],
screenBoundingBoxes: [[], []],
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
},
distancing: {
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
screenFaceOverlap: [0, 0],
screenBoundingBoxes: [
[],
[
{
x: 1,
y: 60,
width: 158,
height: 335
}
]
],
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
},
closing: {
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
screenFaceOverlap: [0, 0],
screenBoundingBoxes: [
[
{
x: 370,
y: 233,
width: 58,
height: 85
}
],
[]
],
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
},
postChallenge: {
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
screenFaceOverlap: [0, 0],
screenBoundingBoxes: [[], []],
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
}
},
plScores: [],
screenDetectionExecutionTimes: {
beforeClickingStart: [],
positioning: Array.from({ length: 2 }, () => randomFloat(5000, 6000)),
liveness: Array.from({ length: 2 }, () => randomFloat(4000, 5000)),
distancing: Array.from({ length: 2 }, () => randomFloat(3000, 4000)),
closing: Array.from({ length: 2 }, () => randomFloat(2000, 3000)),
postChallenge: Array.from({ length: 2 }, () => randomFloat(500, 1500))
},
landmarkDetectionExecutionTimes: {
beforeClickingStart: [],
positioning: Array.from({ length: 200 }, () => randomFloat(50, 150)),
liveness: Array.from({ length: 50 }, () => randomFloat(50, 110)),
distancing: Array.from({ length: 5 }, () => randomFloat(50, 110)),
closing: Array.from({ length: 20 }, () => randomFloat(50, 110)),
postChallenge: Array.from({ length: 7 }, () => randomFloat(50, 110))
},
screenAttackMeasure: 0,
screenAttackBoundingBox: {},
subclient: '1024',
verificationID: jwtPayload.vid,
version: 'v1.10.22',
sdk_path: './face-capture-v1.10.22.js',
model_version: 'v.2025.0',
cropper_version: 'v.0.0.3',
start_time_stamp: currentTime - randomFloat(20, 60),
end_time_stamp: currentTime,
device_timezone: location.timezone,
referring_page: `https://d3ogqhtsivkon3.cloudfront.net/?token=${token}&shi=false&from_qr_scan=true`,
parent_page: `https://d3ogqhtsivkon3.cloudfront.net/dynamic_index.html?sl=${jwtPayload.jti}&region=eu-central-1`,
face_confidence_limit: 0.975,
multipleFacesDetected: false,
targetGate: 18,
targetConfidence: 0.9,
averageAge,
selecedLivenessStyle: 'open',
selectedMediaLabel: 'Front Camera',
rawImageWidth: 480,
rawImageHeight: 640,
boundingBoxesInPixels: Array.from({ length: randomInt(5, 10) }, generateBoundingBox),
latestReportedState: 'AGE_CHECK_COMPLETE',
challengeType: 'distance-open',
authenticationCharacteristics: {
session_id: sessionData.session_id,
session_password: sessionData.session_password,
token
},
deviceCharacteristics: {
deviceBrowserModel: userAgent,
isMobile: parsedUserAgent.device.type === 'mobile',
browserName: parsedUserAgent.browser.name?.toLowerCase() || 'safari',
isDeviceBrowserCompatible: true,
deviceConnectionSpeedKbps: randomFloat(20000, 500000),
deviceRegion: {
country: location.country,
state: location.state
},
mediaMetadata: mediaMetadata,
platformDetails: {
name: parsedUserAgent.browser.name || 'Safari',
version: parsedUserAgent.browser.version || '15.0',
layout: parsedUserAgent.engine.name || 'WebKit',
os: {
architecture: parseInt(parsedUserAgent.cpu.architecture) || 64,
family: parsedUserAgent.os.name || 'iOS',
version: parsedUserAgent.os.version || '15.0'
},
description: `${parsedUserAgent.browser.name || 'Safari'} ${
parsedUserAgent.browser.version || '15.0'
} on ${parsedUserAgent.device.vendor || 'Apple'} ${parsedUserAgent.device.model || 'iPhone'} (${parsedUserAgent.os.name || 'iOS'} ${
parsedUserAgent.os.version || '15.0'
})`,
product: parsedUserAgent.device.model || 'iPhone',
manufacturer: parsedUserAgent.device.vendor || 'Apple'
},
userTriedLandscapeMode: 0,
txFinishedInLandscapeMode: false
},
initializationCharacteristics: {
cropperInitTime: 2718,
coreInitTime: 6746,
pageLoadTime: 602.0999999996275,
from_qr_scan: false,
blendShapesAvailable: true
},
executionCharacteristics: {
experimentSetup: {
experimentType: 'passive-liveness-override',
experimentProbability: 1,
deviceCoverage: 'all',
deviceInfo: {
name: parsedUserAgent.browser.name || 'Safari',
version: parsedUserAgent.browser.version || '15.0',
layout: parsedUserAgent.engine.name || 'WebKit',
os: {
architecture: parseInt(parsedUserAgent.cpu.architecture) || 64,
family: parsedUserAgent.os.name || 'iOS',
version: parsedUserAgent.os.version || '15.0'
},
description: `${parsedUserAgent.browser.name || 'Safari'} ${
parsedUserAgent.browser.version || '15.0'
} on ${parsedUserAgent.device.vendor || 'Apple'} ${parsedUserAgent.device.model || 'iPhone'} (${parsedUserAgent.os.name || 'iOS'} ${
parsedUserAgent.os.version || '15.0'
})`,
product: parsedUserAgent.device.model || 'iPhone',
manufacturer: parsedUserAgent.device.vendor || 'Apple'
},
txMode: 'experiment',
timestamp: new Date().getTime()
},
experimentConfigResult: {
success: true,
txMode: 'experiment',
experimentType: 'passive-liveness-override'
},
isCameraPermissionGranted: true,
completionTime,
deferredComputationStartedAt: randomInt(10000, 14000),
instructionCompletionTime: randomInt(10000, 14000),
initialAdjustmentTime: randomInt(10000, 14000),
completionState: 'COMPLETE',
unfinishedInstructions: Object.fromEntries(
[
'NO_FACE',
'VIDEO_PROCESSING',
'STAY_STILL',
'LOOK_STRAIGHT',
'GET_READY',
'TURN_LEFT',
'TURN_RIGHT',
'ALIGN_YOUR_FACE_WITH_THE_CAMERA_UP',
'ALIGN_YOUR_FACE_WITH_THE_CAMERA_DOWN',
'SLIGHTLY_TILT_YOUR_HEAD_LEFT',
'SLIGHTLY_TILT_YOUR_HEAD_RIGHT',
'CENTRE_FACE',
'OPEN_YOUR_MOUTH',
'KEEP_YOUR_MOUTH_OPEN',
'CLOSE_YOUR_MOUTH',
'SLOWLY_COME_CLOSER_TO_THE_CAMERA',
'SLOWLY_DISTANCE_YOURSELF_FROM_THE_CAMERA',
'TOO_DARK'
].map((k) => [k, false])
),
// "stateCompletionTimes": {
// "TIME_UNTIL_CLICK_START": 2342,
// "GET_READY": 1130,
// "NO_FACE": 7132,
// "CENTRE_FACE": 551,
// "TOO_DARK": 29998,
// "TURN_LEFT": 30133,
// "LOOK_STRAIGHT": 441,
// "SLOWLY_DISTANCE_YOURSELF_FROM_THE_CAMERA": 0,
// "SLOWLY_COME_CLOSER_TO_THE_CAMERA": 1546,
// "CLOSE_YOUR_MOUTH": 1813,
// "KEEP_YOUR_MOUTH_OPEN": 9686
// },
stateCompletionTimes: {
TIME_UNTIL_CLICK_START: randomInt(800, 3200),
GET_READY: randomInt(1200, 4000),
NO_FACE: randomInt(2000, 5500),
CENTRE_FACE: randomInt(8000, 18000),
TOO_DARK: randomInt(1500, 3500),
TURN_LEFT: randomInt(3000, 8500),
LOOK_STRAIGHT: randomInt(100, 800),
SLOWLY_DISTANCE_YOURSELF_FROM_THE_CAMERA: 0,
SLOWLY_COME_CLOSER_TO_THE_CAMERA: randomInt(800, 3500),
CLOSE_YOUR_MOUTH: randomInt(1800, 5200),
KEEP_YOUR_MOUTH_OPEN: randomInt(3500, 9000)
},
stateTimelines: generateStateTimelines(completionTime),
nonNeutralExpressionTimelines: Object.fromEntries(
[
'browDownLeft',
'browDownRight',
'mouthSmileLeft',
'mouthSmileRight',
'mouthPucker',
'mouthDimpleLeft',
'mouthDimpleRight',
'mouthPressLeft',
'mouthPressRight',
'mouthShrugLower',
'mouthShrugUpper',
'eyeBlinkLeft',
'eyeBlinkRight',
'mouthFrownLeft',
'mouthFrownRight'
].map((k) => [k, {}])
),
handAnalysis: {
faceHandSizeComparisons: []
},
predictions: {
outputs,
primaryOutputs,
raws,
secondaryOutputs: [],
secondaryRaws: [],
age: 'yes',
horizontal_estimates: Array.from({ length: 6 }, () => randomFloat(3.1, 3.2)),
vertical_estimates: Array.from({ length: 6 }, () => randomFloat(-1.6, -1.5)),
horizontalratiotocenter_estimates: Array.from({ length: 6 }, () =>
randomFloat(1.01, 1.03)
),
zy_estimates: Array.from({ length: 6 }, () => randomFloat(0.42, 0.44)),
driftfromcenterx_estimates: Array.from({ length: 6 }, () => randomFloat(0.005, 0.007)),
driftfromcentery_estimates: Array.from({ length: 6 }, () => randomFloat(-0.35, -0.37)),
xScaledShiftAmt: 11.5,
yScaledShiftAmt: -2
}
},
errorCharacteristics: {
systemErrors: [],
userErrors: {}
}
}
}
};
const encryptionData = await encryptPayload(sessionData.nonce, payload);
payload = Object.assign(payload, encryptionData);
const completeRes = await fetch(`${BASE_URL}/age-services/d-privately-age-services`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': userAgent,
accept: '*/*',
'accept-language': location.lang,
'access-control-allow-origin': '*',
priority: 'u=1, i',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'cross-site'
},
body: JSON.stringify(payload)
});
if (!completeRes.ok)
throw `failed to complete transaction (status=${completeRes.status}, body=${JSON.stringify(await completeRes.text())})`;
console.log('tx completed', payload.transaction_id);
}
await verify(qrCodeUrl);
+20
View File
@@ -0,0 +1,20 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
interface Platform {
env: Env;
ctx: ExecutionContext;
caches: CacheStorage;
cf?: IncomingRequestCfProperties;
}
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body class="bg-black font-mono text-white" data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.
+7
View File
@@ -0,0 +1,7 @@
<script lang="ts">
import './layout.css';
let { children } = $props();
</script>
{@render children()}
+153
View File
@@ -0,0 +1,153 @@
<svelte:head>
<title>discord age verifier</title>
</svelte:head>
<div class="mx-auto w-screen max-w-6xl items-center p-5 pb-16">
<h1 class="mt-16 text-center text-3xl font-extrabold">discord age verifier</h1>
<p class="text-center">age verifies your discord account automatically as an adult</p>
<p class="mt-4 text-center text-white/50">
made by <a class="underline" href="https://eva.ac" target="_blank">xyzeva</a> and
<a class="underline" href="https://github.com/Dziurwa14" target="_blank">Dziurwa</a>, greetz to
<a class="underline" href="https://amplitudes.me/" target="_blank">amplitudes</a> (for previous work)
</p>
<p class="mt-8 text-left">
it <span class="font-bold">doesn't matter</span> if you are in the UK or similar region that
currently has access to this, this will verify your account for the future global rollout in
march aswell as current. to use, simply paste this script into your discord console by going to
<a
class="font-bold underline"
href="https://discord.com/app"
target="_blank"
rel="noreferer noopener">discord.com/app</a
>, pressing <span class="font-bold">F12</span>, going to <span class="font-bold">Console</span>
and copying and pasting and hitting enter on the following script and solving the captcha that pops
up
<span class="text-white/70">(typing "allow pasting" before if necessary)</span>:
</p>
<pre class="my-4 bg-white/10 p-5 text-wrap">// add a chunk to get all of the webpack chunks
_mods = webpackChunkdiscord_app.push([[Symbol()],&#123;&#125;,r=>r.c]);
webpackChunkdiscord_app.pop(); // cleanup the chunk we added
// utitility to find a webpack chunk by property
findByProps = (...props) => &#123;
for (let m of Object.values(_mods)) &#123;
try &#123;
if (!m.exports || m.exports === window) continue;
if (props.every((x) => m.exports?.[x])) return m.exports;
for (let ex in m.exports) &#123;
if (props.every((x) => m.exports?.[ex]?.[x]) && m.exports[ex][Symbol.toStringTag] !== 'IntlMessagesProxy') return m.exports[ex];
&#125;
&#125; catch &#123;&#125;
&#125;
&#125;
// find the discord api client
api = findByProps('Bo','oh').Bo
// send a api request to discord /age-verification/verify and then redirect the page to our website
window.location.href = `https://discord-verifier.eva.ac/webview?url=$&#123;encodeURIComponent((await api.post(&#123; url: '/age-verification/verify', body: &#123; method: 3 &#125;&#125;)).body.verification_webview_url)&#125;`</pre>
<p class="text-center text-white/50">
(feel free to read the code, we made it readable and we have nothing to hide)
</p>
<p class="mt-4 text-left">
it should navigate to a link <span class="text-white/70"
>(or give you a link to navigate to)</span
>, from there, you can just wait until the page says success
</p>
<p class="mt-2 text-left">congrats! your discord account is now age verified.</p>
<h2 class="mt-12 text-2xl font-bold">how does this work</h2>
<p>
k-id, the age verification provider discord uses doesn't store or send your face to the server.
instead, it sends a bunch of metadata about your face and general process details. while this is
good for your privacy <span class="text-white/50"
>(well, considering some other providers send actual videos of your face to their servers)</span
>, its also bad for them, because we can just send legitimate looking metadata to their servers
and they have no way to tell its not legitimate.
<br />
while this was easy in the past, k-id's partner for face verification (faceassure) has made this significantly
harder to achieve after
<a href="https://github.com/amplitudesxd/discord-k-id-verifier" class="underline"
>amplitudes k-id verifier</a
>
was released,
<span class="text-white/50">(which doesn't work anymore because of it.)</span>
<br />
<br />
with discord's decision of making the age verification requirement global, we decided to look into
it again to see if we can bypass the new checks.
</p>
<h3 class="mt-8 text-xl font-bold">step 1: encrypted_payload and auth_tag</h3>
<p>
the first thing we noticed that the old implementation doesn't send when comparing a legitimate
request payload with a generated one, is its missing <code class="bg-white/20 text-blue-400"
>encrypted_payload</code
>, <code class="bg-white/20 text-blue-400">auth_tag</code>,
<code class="bg-white/20 text-blue-400">timestamp</code>
and
<code class="bg-white/20 text-blue-400">iv</code> in the body.
<br />
<br />
looking at the code, this appears to be a simple AES-GCM cipher with the key being
<code class="bg-white/20"
><span class="text-orange-300">nonce</span> + <span class="text-orange-300">timestamp</span> +
<span class="text-orange-300">transaction_id</span></code
>, derived using HKDF (sha256). we can easily replicate this and also create the missing
parameters in our generated output.
</p>
<h3 class="mt-8 text-xl font-bold">step 2: prediction data</h3>
<p>
heres where it kind of gets tricky, even after perfectly replicating the encryption, our
verification attempt still doesn't succeed, so they must also be doing checks on the actual
payload.
<br />
<br />
after some trial and error, we narrowed the checked part to the prediction arrays, which are
<code class="bg-white/20 text-blue-400">outputs</code>,
<code class="bg-white/20 text-blue-400">primaryOutputs</code>
and <code class="bg-white/20 text-blue-400">raws</code>.
<br />
<br />
turns out, both <code class="bg-white/20 text-blue-400">outputs</code> and
<code class="bg-white/20 text-blue-400">primaryOutputs</code>
are generated from <code class="bg-white/20 text-blue-400">raws</code>. basically, the raw
numbers are mapped to age outputs, and then the outliers get removed with z-score (once for
<code class="bg-white/20 text-blue-400">primaryOutputs</code>
and twice for <code class="bg-white/20 text-blue-400">outputs</code>).
<br />
<br />
there is also some other differences:
</p>
<ul
class="list-inside list-disc
"
>
<li>
<code class="bg-white/20 text-blue-400">xScaledShiftAmt</code> and
<code class="bg-white/20 text-blue-400">yScaledShiftAmt</code> in predictions are not random but
rather can be one of two values
</li>
<li>
it is checked that the media name (camera) matches one of your media devices in the array of
devices
</li>
<li>it is checked if the states completion times match the state timeline</li>
</ul>
<p>
<br />
with all of that done,
<span class="font-extrabold">we can officially verify our age as an adult.</span> all of this
code is open source and available
<a
href="https://github.com/xyzeva/discord-age-verifier"
target="_blank"
class="font-extrabold underline"
rel="noreferer noopener">on github</a
>, so you can actually see how we do this exactly.
</p>
</div>
+794
View File
@@ -0,0 +1,794 @@
import type { RequestEvent } from './$types';
import { Buffer } from 'node:buffer';
const BASE_URL = 'https://eu-west-1.faceassure.com';
const K_ID_DEPLOYMENT_ID = '20260210222654-016f063-production';
const K_ID_TRACK_ACTION = '601d02be14a2f7e7e50a862f6a0585c9b64d928a84';
const K_ID_UPGRADE_TOKEN_ACTION = '40f05c36f2421425f977611b87af923acca2feaaf7';
const K_ID_PRIVATELY_ACTION_ID = '40dc500368168e3130ea4625c535d5a9bbbf0243f1';
const K_ID_NEXT_ROUTER_TREE =
'%5B%22%22%2C%7B%22children%22%3A%5B%22verify%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D';
const jsonResponse = (body: unknown, status: number = 200, extraHeaders: object = {}) =>
new Response(JSON.stringify(body), {
status,
headers: {
'Content-Type': 'application/json',
...extraHeaders
}
});
const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
const randomFloat = (min: number, max: number, decimals = 15) =>
parseFloat((Math.random() * (max - min) + min).toFixed(decimals));
const randomChoice = <T>(arr: T[]): T => arr[Math.floor(Math.random() * arr.length)];
function generateUserAgent() {
const agents = [
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1',
'Mozilla/5.0 (Android 13; Mobile; rv:109.0) Gecko/109.0 Firefox/117.0',
'Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36'
];
return agents[Math.floor(Math.random() * agents.length)];
}
function parseUserAgent(userAgent: string) {
const isIOS = /iPhone|iPad/.test(userAgent);
const isAndroid = /Android/.test(userAgent);
const isSafari = /Safari/.test(userAgent) && !/Chrome/.test(userAgent);
const isChrome = /Chrome/.test(userAgent);
const isFirefox = /Firefox/.test(userAgent);
return {
browser: {
name: isSafari ? 'Safari' : isChrome ? 'Chrome' : isFirefox ? 'Firefox' : 'Safari',
version: isIOS ? '17.0' : '117.0'
},
device: {
type: 'mobile',
vendor: isIOS ? 'Apple' : 'Samsung',
model: isIOS ? 'iPhone' : 'Galaxy'
},
os: {
name: isIOS ? 'iOS' : isAndroid ? 'Android' : 'iOS',
version: isIOS ? '17.0' : '13'
},
engine: { name: isSafari || isIOS ? 'WebKit' : 'Blink' },
cpu: { architecture: '64' }
};
}
function getRandomLocation() {
const locations = [
{
country: 'United States',
state: 'California',
timezone: 'America/Los_Angeles',
lang: 'en-US,en;q=0.9'
},
{
country: 'United States',
state: 'New York',
timezone: 'America/New_York',
lang: 'en-US,en;q=0.9'
},
{
country: 'Canada',
state: 'Ontario',
timezone: 'America/Toronto',
lang: 'en-CA,en;q=0.9,fr;q=0.8'
},
{
country: 'Australia',
state: 'New South Wales',
timezone: 'Australia/Sydney',
lang: 'en-AU,en;q=0.9'
},
{
country: 'Germany',
state: 'Berlin',
timezone: 'Europe/Berlin',
lang: 'de-DE,de;q=0.9,en;q=0.8'
},
{
country: 'France',
state: 'Île-de-France',
timezone: 'Europe/Paris',
lang: 'fr-FR,fr;q=0.9,en;q=0.8'
},
{
country: 'Netherlands',
state: 'North Holland',
timezone: 'Europe/Amsterdam',
lang: 'nl-NL,nl;q=0.9,en;q=0.8'
},
{
country: 'Sweden',
state: 'Stockholm',
timezone: 'Europe/Stockholm',
lang: 'sv-SE,sv;q=0.9,en;q=0.8'
},
{
country: 'Norway',
state: 'Oslo',
timezone: 'Europe/Oslo',
lang: 'nb-NO,nb;q=0.9,en;q=0.8'
}
];
return locations[Math.floor(Math.random() * locations.length)];
}
function generateMediaMetadata() {
const randomHex = () =>
Array.from({ length: 32 }, () =>
Math.floor(Math.random() * 16)
.toString(16)
.toUpperCase()
).join('');
const specs = [
{
width: 4032,
height: 3024,
frameRate: 60,
zoom: 10,
aspectRatio: 4032 / 3024
},
{
width: 3840,
height: 2160,
frameRate: 60,
zoom: 8,
aspectRatio: 3840 / 2160
},
{
width: 1920,
height: 1080,
frameRate: 120,
zoom: 5,
aspectRatio: 1920 / 1080
},
{
width: 2560,
height: 1440,
frameRate: 60,
zoom: 6,
aspectRatio: 2560 / 1440
}
];
const spec = randomChoice(specs);
const deviceId = randomHex();
return [
{
mediaKind: 'audioinput',
mediaLabel: randomChoice(['', 'Built-in Microphone', 'Default']),
mediaId: randomHex(),
mediaCapabilities: {}
},
{
mediaKind: 'videoinput',
mediaLabel: 'Front Camera',
mediaId: deviceId,
mediaCapabilities: {
aspectRatio: {
max: spec.aspectRatio,
min: randomFloat(0.0003, 0.001, 15)
},
backgroundBlur: [false],
deviceId: deviceId,
facingMode: ['user'],
focusDistance: { min: randomFloat(0.1, 0.3) },
frameRate: { max: spec.frameRate, min: 1 },
groupId: randomHex(),
height: { max: spec.height, min: 1 },
powerEfficient: [false, true],
whiteBalanceMode: randomChoice([
['manual', 'continuous'],
['auto', 'manual'],
['continuous']
]),
width: { max: spec.width, min: 1 },
zoom: { max: spec.zoom, min: 1 }
}
}
];
}
const AMAP_MAP: Record<number, [number, number]> = {
0: [0, 2],
1: [2, 4],
2: [4, 8],
3: [8, 13],
4: [13, 18],
5: [18, 21],
6: [21, 25],
7: [25, 28],
8: [28, 32],
9: [32, 36],
10: [36, 40],
11: [40, 45],
12: [45, 50],
13: [50, 60],
14: [60, 70],
15: [70, 120]
};
function amap(e: number) {
const n = AMAP_MAP[~~e];
const r = e % 1;
return n[0] + r * (n[1] - n[0]);
}
function removeOutliersWithZscore(arr: number[]) {
const r =
arr.reduce(function (e, t) {
return e + t;
}, 0) / arr.length;
const a =
arr.reduce(function (e, t) {
return e + Math.pow(t - r, 2);
}, 0) / arr.length;
const s = Math.sqrt(a);
return arr.filter(function (e) {
return Math.abs((e - r) / s) <= 1;
});
}
async function encryptPayload(nonce: string, payload: any) {
const getKey = async (nonce: string, timestamp: string, transactionId: string) => {
const data = nonce + timestamp + transactionId;
const dataEncoded = new TextEncoder().encode(data);
const key = await crypto.subtle.importKey(
'raw',
dataEncoded,
{
name: 'HKDF'
},
false,
['deriveBits']
);
const derived = await crypto.subtle.deriveBits(
{
name: 'HKDF',
hash: 'SHA-256',
salt: new Uint8Array(0),
info: new TextEncoder().encode('payload-encryption')
},
key,
32 * 8
);
return await crypto.subtle.importKey(
'raw',
derived,
{
name: 'AES-GCM'
},
false,
['encrypt']
);
};
const timestamp = new Date().toISOString();
const key = await getKey(nonce, timestamp, payload.transaction_id);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encryptedBuffer = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
new TextEncoder().encode(JSON.stringify(payload))
);
const rawBuffer = Buffer.from(encryptedBuffer);
const encryptedPayloadBuf = rawBuffer.subarray(0, rawBuffer.length - 16);
const authTagBuf = rawBuffer.subarray(rawBuffer.length - 16);
return {
encrypted_payload: encryptedPayloadBuf.toString('base64'),
iv: Buffer.from(iv).toString('base64'),
auth_tag: authTagBuf.toString('base64'),
timestamp
};
}
async function verify(
userAgent: string,
location: ReturnType<typeof getRandomLocation>,
token: string
) {
const parsedUserAgent = parseUserAgent(userAgent);
const mediaMetadata = generateMediaMetadata();
const commonHeaders = {
'User-Agent': userAgent,
accept: '*/*',
'accept-language': location.lang,
'access-control-allow-origin': '*',
priority: 'u=1, i',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'cross-site'
};
/*
const qrCodeUrl = new URL(qrCodeUrlStr);
const shortlinkId = qrCodeUrl.searchParams.get('sl');
if (!shortlinkId) throw `shortlink id not found in qr code url`;
const res = await fetch(`${BASE_URL}/shortlinks/${shortlinkId}`, {
headers: commonHeaders
});
if (!res.ok)
`failed to get shortlink (status=${res.status}, body=${JSON.stringify(await res.text())})`;
const data = await res.json();
const originalUrl = new URL(data.Item.original_url.S.replace('#', ''));
const token = originalUrl.searchParams.get('token');
if (!token) throw `token not found in original url`;
*/
const parts = token.split('.');
if (parts.length !== 3) throw `token is an invalid jwt `;
const jwtPayload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
const sessionRes = await fetch(`${BASE_URL}/age-services/d-privately-age-services`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...commonHeaders
},
body: JSON.stringify({
request_type: 'generate_new_session',
transaction_id: jwtPayload.jti,
api_key: null,
api_secret: null,
token,
longURL: null,
userAgent: userAgent
})
});
if (!sessionRes.ok)
throw `failed to generate new session (status=${sessionRes.status}, body=${JSON.stringify(await sessionRes.text())})`;
const sessionData = await sessionRes.json();
const generateBoundingBox = () => {
const topLeft = [randomFloat(140, 160), randomFloat(250, 270)];
const width = randomFloat(170, 190);
const height = randomFloat(220, 240);
return {
topLeft,
bottomRight: [topLeft[0] + width, topLeft[1] + height],
width,
height
};
};
const generateTimeline = (maxTime: number) => {
const entries = [];
for (let i = 0; i < randomInt(2, 5); i++) {
const start = randomInt(1, maxTime - 100);
const end = start + randomInt(50, 500);
if (end < maxTime) entries.push([start, end]);
}
return entries;
};
const generateStateTimelines = (completionTime: number) => {
const states = [
'TIME_UNTIL_CLICK_START',
'GET_READY',
'NO_FACE',
'LOOK_STRAIGHT',
'TURN_LEFT',
'CENTRE_FACE',
'KEEP_YOUR_MOUTH_OPEN',
'CLOSE_YOUR_MOUTH',
'SLOWLY_COME_CLOSER_TO_THE_CAMERA',
'SLOWLY_DISTANCE_YOURSELF_FROM_THE_CAMERA',
'TOO_DARK'
];
const timelines: Record<string, number[][]> = {};
states.forEach((state) => (timelines[state] = generateTimeline(completionTime)));
return timelines;
};
const baseAge = randomFloat(25.2, 26.0);
const minAge = baseAge - randomFloat(0.1, 0.5);
const maxAge = baseAge + randomFloat(0.1, 0.5);
const averageAge = (minAge + maxAge) / 2;
const currentTime = Date.now() / 1000;
const completionTime = randomInt(8000, 15000);
const raws = Array.from({ length: 10 }, () => randomFloat(6.005, 7.007));
const primaryOutputs = removeOutliersWithZscore(raws.map((r) => amap(r)));
const outputs = removeOutliersWithZscore(primaryOutputs);
let payload = {
request_type: 'complete_transaction',
transaction_id: sessionData.transaction_id,
api_key: sessionData.session_id,
api_secret: sessionData.session_password,
remote_pld: {},
browser_response_data: {
age: 'yes',
age_confidence: 1,
genuineness: Array.from({ length: 5 }, () => randomFloat(0.4, 0.98)),
product: 'age',
modality: 'image',
unverifiedPayload: {
iss: 'https://api.privately.swiss',
sub: '1024',
aud: 'https://api.k-id.com',
exp: jwtPayload.exp,
nbf: jwtPayload.nbf,
iat: jwtPayload.iat,
jti: jwtPayload.jti,
age: jwtPayload.age,
liv: true,
rlt: {
minAge,
maxAge,
score: 0,
gate: 16
},
rsn: 'complete_transaction',
rtf: 'interval',
rtb: 'callback',
vid: jwtPayload.vid,
ver: 'v1.10.22',
ufi: []
},
ageCheckSession: '-' + randomInt(1000000000, 9999999999),
miscellaneous: {
recordedOpennessStreak: Array.from({ length: 5 }, () => randomFloat(0.1, 0.8, 17)),
recordedSpeeds: Array.from({ length: 5 }, () => randomFloat(0.2, 1.5, 17)),
recordedIntervals: Array.from({ length: 5 }, () => randomFloat(0.1, 0.2, 3)),
failedOpennessReadings: [],
failedOpennessSpeeds: [],
failedOpennessIntervals: [],
numberOfGestureRetries: 1,
antiSpoofConfidences: [],
fp_scores: [],
laplacian_blur_scores: Array.from({ length: 300 }, () => randomFloat(10, 300, 15)),
laplacian_min_score: randomFloat(10, 50),
laplacian_max_score: randomFloat(300, 350),
laplacian_avg_score: randomFloat(50, 100),
glare_ratios: Array.from({ length: 300 }, () => 0),
allScreenDetectionDetails: {
beforeClickingStart: {
screenDetectionConfidence: [],
screenFaceOverlap: [],
screenBoundingBoxes: [],
alternativeScore: []
},
positioning: {
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
screenFaceOverlap: [0, 0],
screenBoundingBoxes: [[], []],
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
},
liveness: {
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
screenFaceOverlap: [0, 0],
screenBoundingBoxes: [[], []],
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
},
distancing: {
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
screenFaceOverlap: [0, 0],
screenBoundingBoxes: [
[],
[
{
x: 1,
y: 60,
width: 158,
height: 335
}
]
],
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
},
closing: {
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
screenFaceOverlap: [0, 0],
screenBoundingBoxes: [
[
{
x: 370,
y: 233,
width: 58,
height: 85
}
],
[]
],
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
},
postChallenge: {
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
screenFaceOverlap: [0, 0],
screenBoundingBoxes: [[], []],
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
}
},
plScores: [],
screenDetectionExecutionTimes: {
beforeClickingStart: [],
positioning: Array.from({ length: 2 }, () => randomFloat(5000, 6000)),
liveness: Array.from({ length: 2 }, () => randomFloat(4000, 5000)),
distancing: Array.from({ length: 2 }, () => randomFloat(3000, 4000)),
closing: Array.from({ length: 2 }, () => randomFloat(2000, 3000)),
postChallenge: Array.from({ length: 2 }, () => randomFloat(500, 1500))
},
landmarkDetectionExecutionTimes: {
beforeClickingStart: [],
positioning: Array.from({ length: 200 }, () => randomFloat(50, 150)),
liveness: Array.from({ length: 50 }, () => randomFloat(50, 110)),
distancing: Array.from({ length: 5 }, () => randomFloat(50, 110)),
closing: Array.from({ length: 20 }, () => randomFloat(50, 110)),
postChallenge: Array.from({ length: 7 }, () => randomFloat(50, 110))
},
screenAttackMeasure: 0,
screenAttackBoundingBox: {},
subclient: '1024',
verificationID: jwtPayload.vid,
version: 'v1.10.22',
sdk_path: './face-capture-v1.10.22.js',
model_version: 'v.2025.0',
cropper_version: 'v.0.0.3',
start_time_stamp: currentTime - randomFloat(20, 60),
end_time_stamp: currentTime,
device_timezone: location.timezone,
referring_page: `https://d3ogqhtsivkon3.cloudfront.net/?token=${token}&shi=false&from_qr_scan=true`,
parent_page: `https://d3ogqhtsivkon3.cloudfront.net/dynamic_index.html?sl=${jwtPayload.jti}&region=eu-central-1`,
face_confidence_limit: 0.975,
multipleFacesDetected: false,
targetGate: 18,
targetConfidence: 0.9,
averageAge,
selecedLivenessStyle: 'open',
selectedMediaLabel: 'Front Camera',
rawImageWidth: 480,
rawImageHeight: 640,
boundingBoxesInPixels: Array.from({ length: randomInt(5, 10) }, generateBoundingBox),
latestReportedState: 'AGE_CHECK_COMPLETE',
challengeType: 'distance-open',
authenticationCharacteristics: {
session_id: sessionData.session_id,
session_password: sessionData.session_password,
token
},
deviceCharacteristics: {
deviceBrowserModel: userAgent,
isMobile: parsedUserAgent.device.type === 'mobile',
browserName: parsedUserAgent.browser.name?.toLowerCase() || 'safari',
isDeviceBrowserCompatible: true,
deviceConnectionSpeedKbps: randomFloat(20000, 500000),
deviceRegion: {
country: location.country,
state: location.state
},
mediaMetadata: mediaMetadata,
platformDetails: {
name: parsedUserAgent.browser.name || 'Safari',
version: parsedUserAgent.browser.version || '15.0',
layout: parsedUserAgent.engine.name || 'WebKit',
os: {
architecture: parseInt(parsedUserAgent.cpu.architecture) || 64,
family: parsedUserAgent.os.name || 'iOS',
version: parsedUserAgent.os.version || '15.0'
},
description: `${parsedUserAgent.browser.name || 'Safari'} ${
parsedUserAgent.browser.version || '15.0'
} on ${parsedUserAgent.device.vendor || 'Apple'} ${parsedUserAgent.device.model || 'iPhone'} (${parsedUserAgent.os.name || 'iOS'} ${
parsedUserAgent.os.version || '15.0'
})`,
product: parsedUserAgent.device.model || 'iPhone',
manufacturer: parsedUserAgent.device.vendor || 'Apple'
},
userTriedLandscapeMode: 0,
txFinishedInLandscapeMode: false
},
initializationCharacteristics: {
cropperInitTime: 2718,
coreInitTime: 6746,
pageLoadTime: 602.0999999996275,
from_qr_scan: false,
blendShapesAvailable: true
},
executionCharacteristics: {
experimentSetup: {
experimentType: 'passive-liveness-override',
experimentProbability: 1,
deviceCoverage: 'all',
deviceInfo: {
name: parsedUserAgent.browser.name || 'Safari',
version: parsedUserAgent.browser.version || '15.0',
layout: parsedUserAgent.engine.name || 'WebKit',
os: {
architecture: parseInt(parsedUserAgent.cpu.architecture) || 64,
family: parsedUserAgent.os.name || 'iOS',
version: parsedUserAgent.os.version || '15.0'
},
description: `${parsedUserAgent.browser.name || 'Safari'} ${
parsedUserAgent.browser.version || '15.0'
} on ${parsedUserAgent.device.vendor || 'Apple'} ${parsedUserAgent.device.model || 'iPhone'} (${parsedUserAgent.os.name || 'iOS'} ${
parsedUserAgent.os.version || '15.0'
})`,
product: parsedUserAgent.device.model || 'iPhone',
manufacturer: parsedUserAgent.device.vendor || 'Apple'
},
txMode: 'experiment',
timestamp: new Date().getTime()
},
experimentConfigResult: {
success: true,
txMode: 'experiment',
experimentType: 'passive-liveness-override'
},
isCameraPermissionGranted: true,
completionTime,
deferredComputationStartedAt: randomInt(10000, 14000),
instructionCompletionTime: randomInt(10000, 14000),
initialAdjustmentTime: randomInt(10000, 14000),
completionState: 'COMPLETE',
unfinishedInstructions: Object.fromEntries(
[
'NO_FACE',
'VIDEO_PROCESSING',
'STAY_STILL',
'LOOK_STRAIGHT',
'GET_READY',
'TURN_LEFT',
'TURN_RIGHT',
'ALIGN_YOUR_FACE_WITH_THE_CAMERA_UP',
'ALIGN_YOUR_FACE_WITH_THE_CAMERA_DOWN',
'SLIGHTLY_TILT_YOUR_HEAD_LEFT',
'SLIGHTLY_TILT_YOUR_HEAD_RIGHT',
'CENTRE_FACE',
'OPEN_YOUR_MOUTH',
'KEEP_YOUR_MOUTH_OPEN',
'CLOSE_YOUR_MOUTH',
'SLOWLY_COME_CLOSER_TO_THE_CAMERA',
'SLOWLY_DISTANCE_YOURSELF_FROM_THE_CAMERA',
'TOO_DARK'
].map((k) => [k, false])
),
// "stateCompletionTimes": {
// "TIME_UNTIL_CLICK_START": 2342,
// "GET_READY": 1130,
// "NO_FACE": 7132,
// "CENTRE_FACE": 551,
// "TOO_DARK": 29998,
// "TURN_LEFT": 30133,
// "LOOK_STRAIGHT": 441,
// "SLOWLY_DISTANCE_YOURSELF_FROM_THE_CAMERA": 0,
// "SLOWLY_COME_CLOSER_TO_THE_CAMERA": 1546,
// "CLOSE_YOUR_MOUTH": 1813,
// "KEEP_YOUR_MOUTH_OPEN": 9686
// },
stateCompletionTimes: {
TIME_UNTIL_CLICK_START: randomInt(800, 3200),
GET_READY: randomInt(1200, 4000),
NO_FACE: randomInt(2000, 5500),
CENTRE_FACE: randomInt(8000, 18000),
TOO_DARK: randomInt(1500, 3500),
TURN_LEFT: randomInt(3000, 8500),
LOOK_STRAIGHT: randomInt(100, 800),
SLOWLY_DISTANCE_YOURSELF_FROM_THE_CAMERA: 0,
SLOWLY_COME_CLOSER_TO_THE_CAMERA: randomInt(800, 3500),
CLOSE_YOUR_MOUTH: randomInt(1800, 5200),
KEEP_YOUR_MOUTH_OPEN: randomInt(3500, 9000)
},
stateTimelines: generateStateTimelines(completionTime),
nonNeutralExpressionTimelines: Object.fromEntries(
[
'browDownLeft',
'browDownRight',
'mouthSmileLeft',
'mouthSmileRight',
'mouthPucker',
'mouthDimpleLeft',
'mouthDimpleRight',
'mouthPressLeft',
'mouthPressRight',
'mouthShrugLower',
'mouthShrugUpper',
'eyeBlinkLeft',
'eyeBlinkRight',
'mouthFrownLeft',
'mouthFrownRight'
].map((k) => [k, {}])
),
handAnalysis: {
faceHandSizeComparisons: []
},
predictions: {
outputs,
primaryOutputs,
raws,
secondaryOutputs: [],
secondaryRaws: [],
age: 'yes',
horizontal_estimates: Array.from({ length: 6 }, () => randomFloat(3.1, 3.2)),
vertical_estimates: Array.from({ length: 6 }, () => randomFloat(-1.6, -1.5)),
horizontalratiotocenter_estimates: Array.from({ length: 6 }, () =>
randomFloat(1.01, 1.03)
),
zy_estimates: Array.from({ length: 6 }, () => randomFloat(0.42, 0.44)),
driftfromcenterx_estimates: Array.from({ length: 6 }, () => randomFloat(0.005, 0.007)),
driftfromcentery_estimates: Array.from({ length: 6 }, () => randomFloat(-0.35, -0.37)),
xScaledShiftAmt: 11.5,
yScaledShiftAmt: -2
}
},
errorCharacteristics: {
systemErrors: [],
userErrors: {}
}
}
}
};
const encryptionData = await encryptPayload(sessionData.nonce, payload);
payload = Object.assign(payload, encryptionData);
const completeRes = await fetch(`${BASE_URL}/age-services/d-privately-age-services`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': userAgent,
accept: '*/*',
'accept-language': location.lang,
'access-control-allow-origin': '*',
priority: 'u=1, i',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'cross-site'
},
body: JSON.stringify(payload)
});
if (!completeRes.ok)
throw `failed to complete transaction (status=${completeRes.status}, body=${JSON.stringify(await completeRes.text())})`;
console.log('tx completed', payload.transaction_id);
}
export const POST = async (event: RequestEvent) => {
const userAgent = generateUserAgent();
const location = getRandomLocation();
const { type, identifier }: { type: string; identifier: string } = await event.request.json();
if (type === 'webview') {
const webviewUrl = new URL(identifier);
if (webviewUrl.host !== 'family.k-id.com' || webviewUrl.pathname !== '/verify') {
return jsonResponse({ error: 'unexpected webview url' }, 400);
}
const kIdToken = webviewUrl.searchParams.get('token');
if (!kIdToken) {
return jsonResponse({ error: 'no k-id token in webview url' }, 400);
}
const parts = kIdToken.split('.');
if (parts.length !== 3) {
return jsonResponse({ error: 'invalid k-id jwt' }, 400);
}
const payload = JSON.parse(atob(parts[1]));
// todo: implement getting a privately token from a k-id token
return jsonResponse({ error: 'not implemented' }, 418);
}
return jsonResponse({ error: 'unexpected type' }, 400);
};
+1
View File
@@ -0,0 +1 @@
@import 'tailwindcss';
+46
View File
@@ -0,0 +1,46 @@
<script lang="ts">
import { page } from '$app/state';
import { onMount } from 'svelte';
const webviewUrl = page.url.searchParams.get('url');
let success = $state(false);
let error = $state<string | null>(null);
onMount(() => {
if (!webviewUrl) return;
fetch('/api/verify', {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ type: 'webview', identifier: webviewUrl })
})
.then(async (r) => {
if (!r.ok) {
error = await r.text().catch(() => 'unexpected error');
return;
}
success = true;
})
.catch((e) => {
console.error('error sending verify request', e);
error = 'unexpected error, please check your browser console.';
});
});
</script>
<div class="flex min-h-screen w-screen items-center justify-center text-3xl">
{#if webviewUrl}
{#if success}
<p class="text-green-500">your account is now age verified. enjoy</p>
{:else if error}
<p class="text-red-500">{error}</p>
{:else}
<p>automatically verifying your age</p>
{/if}
{:else}
<p>invalid url</p>
{/if}
</div>
+3
View File
@@ -0,0 +1,3 @@
# allow crawling everything by default
User-agent: *
Disallow:
+6
View File
@@ -0,0 +1,6 @@
import adapter from '@sveltejs/adapter-cloudflare';
/** @type {import('@sveltejs/kit').Config} */
const config = { kit: { adapter: adapter() } };
export default config;
+16
View File
@@ -0,0 +1,16 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"rewriteRelativeImportExtensions": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler",
"types": ["./worker-configuration.d.ts"]
}
}
+5
View File
@@ -0,0 +1,5 @@
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({ plugins: [tailwindcss(), sveltekit()] });
+13
View File
@@ -0,0 +1,13 @@
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "discord-verifier",
"compatibility_date": "2026-02-10",
"compatibility_flags": ["nodejs_als"],
"main": ".svelte-kit/cloudflare/_worker.js",
"assets": {
"binding": "ASSETS",
"directory": ".svelte-kit/cloudflare"
},
"workers_dev": true,
"preview_urls": true
}