Initial commit
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
||||
|
||||
export const nowIso = () => new Date().toISOString();
|
||||
export const randomId = (bytes = 16) => randomBytes(bytes).toString('hex');
|
||||
export const randomToken = () => `kv_${randomBytes(24).toString('base64url')}`;
|
||||
export const tokenHash = (token) => createHash('sha256').update(token).digest('hex');
|
||||
export const sha256 = (input) => createHash('sha256').update(input).digest('hex');
|
||||
|
||||
export function safeEqualHex(a, b) {
|
||||
if (!a || !b || a.length !== b.length) return false;
|
||||
return timingSafeEqual(Buffer.from(a, 'hex'), Buffer.from(b, 'hex'));
|
||||
}
|
||||
|
||||
export function cleanText(value, max = 500) {
|
||||
if (value == null) return null;
|
||||
const s = String(value).replace(/[\u0000-\u001f\u007f]/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
return s.slice(0, max) || null;
|
||||
}
|
||||
|
||||
export function asInt(value, { min = 0, max = Number.MAX_SAFE_INTEGER, fallback = 0 } = {}) {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n)) return fallback;
|
||||
const i = Math.trunc(n);
|
||||
return Math.max(min, Math.min(max, i));
|
||||
}
|
||||
|
||||
function validIsbn10(value) {
|
||||
if (!/^\d{9}[\dX]$/.test(value)) return false;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const d = value[i] === 'X' ? 10 : Number(value[i]);
|
||||
sum += d * (10 - i);
|
||||
}
|
||||
return sum % 11 === 0;
|
||||
}
|
||||
|
||||
function validIsbn13(value) {
|
||||
if (!/^97[89]\d{10}$/.test(value)) return false;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 12; i++) sum += Number(value[i]) * (i % 2 ? 3 : 1);
|
||||
return (10 - (sum % 10)) % 10 === Number(value[12]);
|
||||
}
|
||||
|
||||
/** Extract a validated ISBN from EPUB/KOReader identifier metadata.
|
||||
* KOReader's doc_props.identifiers is commonly a newline-separated string such as
|
||||
* `isbn:978...`, `urn:isbn:...`, `calibre:...`, `uuid:...`.
|
||||
*/
|
||||
export function extractIsbn(value) {
|
||||
if (!value) return null;
|
||||
const raw = String(value).toUpperCase();
|
||||
const candidates = raw.match(/(?:97[89][\d\s-]{9,20}\d|\d[\d\s-]{7,16}[\dX])/g) || [];
|
||||
const cleaned = [...new Set(candidates.map(x => x.replace(/[^0-9X]/g, '')))];
|
||||
const isbn13 = cleaned.find(x => x.length === 13 && validIsbn13(x));
|
||||
if (isbn13) return isbn13;
|
||||
return cleaned.find(x => x.length === 10 && validIsbn10(x)) || null;
|
||||
}
|
||||
|
||||
export function json(res, status, body, extraHeaders = {}) {
|
||||
const payload = Buffer.from(JSON.stringify(body));
|
||||
res.writeHead(status, {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Content-Length': payload.length,
|
||||
'Cache-Control': 'no-store',
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
...extraHeaders,
|
||||
});
|
||||
res.end(payload);
|
||||
}
|
||||
Reference in New Issue
Block a user