Initial import: live state on api.qbirr.com (server v0.6.3)

This commit is contained in:
2026-05-26 16:06:29 +02:00
commit 7ba4cb4a31
38 changed files with 5242 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
/**
* Self-contained crypto helpers — no native deps.
* - Password hashing via scrypt (built-in).
* - HS256 JWTs for dashboard sessions.
* - Random license keys / ids.
*/
import {
randomBytes,
randomUUID,
scryptSync,
timingSafeEqual,
createHmac,
} from 'node:crypto';
import { config } from '../config.js';
// ---- password hashing (scrypt) ----
export function hashPassword(password: string): string {
const salt = randomBytes(16);
const hash = scryptSync(password, salt, 64);
return `scrypt$${salt.toString('hex')}$${hash.toString('hex')}`;
}
export function verifyPassword(password: string, stored: string): boolean {
const parts = stored.split('$');
if (parts.length !== 3 || parts[0] !== 'scrypt') return false;
const salt = Buffer.from(parts[1]!, 'hex');
const expected = Buffer.from(parts[2]!, 'hex');
const actual = scryptSync(password, salt, expected.length);
return actual.length === expected.length && timingSafeEqual(actual, expected);
}
// ---- ids / keys ----
export function newId(): string {
return randomUUID();
}
/** License key like "wpide_live_<32hex>". */
export function newLicenseKey(): string {
return `wpide_live_${randomBytes(16).toString('hex')}`;
}
/** Opaque random token (oauth state, etc.). */
export function randomToken(bytes = 24): string {
return randomBytes(bytes).toString('base64url');
}
// ---- minimal HS256 JWT ----
function b64url(input: Buffer | string): string {
return Buffer.from(input).toString('base64url');
}
export function signJwt(payload: Record<string, unknown>, ttlSeconds = 60 * 60 * 24 * 30): string {
const header = { alg: 'HS256', typ: 'JWT' };
const now = Math.floor(Date.now() / 1000);
const body = { ...payload, iat: now, exp: now + ttlSeconds };
const head = b64url(JSON.stringify(header));
const data = b64url(JSON.stringify(body));
const sig = createHmac('sha256', config.JWT_SECRET).update(`${head}.${data}`).digest('base64url');
return `${head}.${data}.${sig}`;
}
export function verifyJwt<T = Record<string, unknown>>(token: string): T | null {
const parts = token.split('.');
if (parts.length !== 3) return null;
const [head, data, sig] = parts as [string, string, string];
const expected = createHmac('sha256', config.JWT_SECRET).update(`${head}.${data}`).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(data, 'base64url').toString('utf8')) as { exp?: number };
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) return null;
return payload as T;
} catch {
return null;
}
}