Privacy Score — TypeScript source
One number for your privacy health: your browser fingerprint, a test password's strength and breach exposure, and a site's security headers — four checks, one score, concrete fixes. Runs in your browser; only a 5-character hash prefix ever leaves it.
This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Privacy Score (FEAT-030) — the composite scoring engine. Pure and
// null-safe: every category of PrivacyScoreInput is optional; run
// categories score out of 25 each and the overall percent is normalized
// over the categories that actually ran (skipping a check never lowers
// your score). Mirrored in Go at cli/privacy-score (lock-step vectors).
export type HeaderGrade = 'A' | 'B' | 'C' | 'F';
export interface PrivacyScoreInput {
/** What browser-fingerprint actually produces: signal counts by risk. */
fingerprint?: { signalCount: number; highRiskCount: number; mediumRiskCount: number };
password?: { score: number; breached: boolean };
headers?: { grades: HeaderGrade[] };
breach?: { pwned: boolean };
}
export type CategoryStatus = 'ok' | 'warn' | 'bad';
export interface Recommendation {
text: string;
toolSlug: string;
}
export interface CategoryResult {
id: 'fingerprint' | 'password' | 'headers' | 'breach';
label: string;
points: number;
max: 25;
status: CategoryStatus;
recommendation?: Recommendation;
toolSlug: string;
}
export interface PrivacyScoreResult {
total: number;
max: number;
percent: number;
letter: 'A' | 'B' | 'C' | 'D' | '—';
categories: CategoryResult[];
}
const TOOLS = {
fingerprint: 'browser-fingerprint',
password: 'password-strength-analyser',
headers: 'security-headers',
breach: 'breach-checker',
} as const;
function finalize(
id: CategoryResult['id'],
label: string,
points: number,
okText: string,
warnText: string,
badText: string
): CategoryResult {
const clamped = Math.max(0, Math.min(25, Math.round(points)));
const status: CategoryStatus = clamped >= 20 ? 'ok' : clamped >= 10 ? 'warn' : 'bad';
const text = status === 'ok' ? okText : status === 'warn' ? warnText : badText;
return {
id,
label,
points: clamped,
max: 25,
status,
recommendation: status === 'ok' ? undefined : { text, toolSlug: TOOLS[id] },
toolSlug: TOOLS[id],
};
}
function fingerprintPoints(signalCount: number, highRiskCount: number, mediumRiskCount: number): number {
// Honesty note: the tool measures WHICH signals exist, not real entropy.
// More distinguishable signals — especially high-risk ones (GPU string,
// fonts list) — make a browser more unique, so the score degrades with
// each: high-risk signals cost 4 each, medium 1.5, and a large total
// surface costs a little more.
const surface = Math.max(0, signalCount - 12) * 0.5;
return Math.max(0, 25 - highRiskCount * 4 - mediumRiskCount * 1.5 - surface);
}
function passwordPoints(score: number, breached: boolean): number {
const base = (Math.max(0, Math.min(4, score)) / 4) * 25;
// Breached is urgent regardless of strength: the ×0.32 floor puts even a
// score-4 password at 8 points → 'bad' status → the change-it recommendation.
return breached ? base * 0.32 : base;
}
function headersPoints(grades: HeaderGrade[]): number {
const counts = { A: 0, B: 0, C: 0, F: 0 } as Record<HeaderGrade, number>;
for (const g of grades) if (g in counts) counts[g]++;
if (counts.F > 0) return 0;
if (counts.C > 0) return 10;
if (counts.B > 0) return 18;
return grades.length > 0 ? 25 : 0;
}
function letterFor(percent: number, max: number): PrivacyScoreResult['letter'] {
if (max === 0) return '—';
if (percent >= 85) return 'A';
if (percent >= 70) return 'B';
if (percent >= 50) return 'C';
return 'D';
}
/** Score the checks that ran. Order is stable: fingerprint, password, headers, breach. */
export function scorePrivacy(input: PrivacyScoreInput): PrivacyScoreResult {
const categories: CategoryResult[] = [];
if (input.fingerprint) {
categories.push(
finalize(
'fingerprint',
'Browser fingerprint',
fingerprintPoints(
input.fingerprint.signalCount,
input.fingerprint.highRiskCount,
input.fingerprint.mediumRiskCount
),
'Your browser blends into the crowd.',
'Some signals make your browser fairly unique.',
'Your browser is highly identifiable — consider hardening.'
)
);
}
if (input.password) {
categories.push(
finalize(
'password',
'Password strength',
passwordPoints(input.password.score, input.password.breached),
'Strong test password.',
'The test password could be stronger.',
'Weak or breached — change it everywhere it was reused.'
)
);
}
if (input.headers) {
categories.push(
finalize(
'headers',
'Site security headers',
headersPoints(input.headers.grades),
'The site sends best-practice headers.',
'Some headers are weak or missing.',
'A dangerous or missing header undermines the site.'
)
);
}
if (input.breach) {
categories.push(
finalize(
'breach',
'Breach exposure',
input.breach.pwned ? 0 : 25,
'The tested password is not in known breaches.',
'The tested password appears in breaches.',
'That password is exposed in known breaches — stop using it now.'
)
);
}
const total = categories.reduce((a, c) => a + c.points, 0);
const max = categories.length * 25;
const percent = max === 0 ? 0 : Math.round((total / max) * 100);
return { total, max, percent, letter: letterFor(percent, max), categories };
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →