Files
GDPR-Content-Blocker/license-backend/src/util.js
s4luorth ecb5e1bd22 chore: monorepo - plugin, backend und hilfsdaten in einem repo
- Eltern-Ordner ist jetzt EIN Git-Repo (statt getrennter Repos).
- root .gitignore haelt Secrets (.env), node_modules, DB und Build-Artefakte raus.
- release.ps1: manueller Release (ZIP bauen + ans Backend laden).
- root README mit Struktur und Release-Ablauf.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:41:38 +02:00

93 lines
2.9 KiB
JavaScript

import { randomInt, timingSafeEqual, createHmac } from 'node:crypto';
// Unambiguous alphabet (no 0/O/1/I/L) for human-readable, dictation-safe keys.
const ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
/** Generate a license key like ABCD-EFGH-JKMN-PQRS. */
export function generateKey(groups = 4, groupLen = 4) {
const parts = [];
for (let g = 0; g < groups; g++) {
let s = '';
for (let i = 0; i < groupLen; i++) {
s += ALPHABET[randomInt(0, ALPHABET.length)];
}
parts.push(s);
}
return parts.join('-');
}
/**
* Normalize a domain for stable comparison:
* strip scheme, path, port, leading www, lowercase.
*/
export function normalizeDomain(input) {
if (!input || typeof input !== 'string') return '';
let d = input.trim().toLowerCase();
d = d.replace(/^[a-z]+:\/\//, ''); // scheme
d = d.replace(/\/.*$/, ''); // path
d = d.replace(/:.*$/, ''); // port
d = d.replace(/^www\./, ''); // www
return d;
}
/** Timing-safe string compare for tokens. */
export function safeEqual(a, b) {
const ba = Buffer.from(String(a || ''));
const bb = Buffer.from(String(b || ''));
if (ba.length !== bb.length) return false;
return timingSafeEqual(ba, bb);
}
export function nowIso() {
return new Date().toISOString();
}
/** True if an ISO date string is in the past. */
export function isExpired(expiresAt) {
if (!expiresAt) return false;
const t = Date.parse(expiresAt);
if (Number.isNaN(t)) return false;
return t < Date.now();
}
/** Compare dotted version strings. Returns 1 if a>b, -1 if a<b, 0 if equal. */
export function compareVersions(a, b) {
const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0);
const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0);
const len = Math.max(pa.length, pb.length);
for (let i = 0; i < len; i++) {
const x = pa[i] || 0;
const y = pb[i] || 0;
if (x > y) return 1;
if (x < y) return -1;
}
return 0;
}
/** Sign a small JSON payload (download tokens). Returns "body.sig" (base64url). */
export function signToken(payload, secret) {
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
const sig = createHmac('sha256', secret).update(body).digest('base64url');
return body + '.' + sig;
}
/** Verify a token from signToken. Returns the payload, or null if bad/expired. */
export function verifyToken(token, secret) {
if (typeof token !== 'string' || !token.includes('.')) return null;
const [body, sig] = token.split('.');
if (!body || !sig) return null;
const expected = createHmac('sha256', secret).update(body).digest('base64url');
const a = Buffer.from(sig);
const b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) return null;
try {
const payload = JSON.parse(Buffer.from(body, 'base64url').toString('utf8'));
if (payload.exp && Date.now() > payload.exp) return null;
return payload;
} catch {
return null;
}
}