Files
suaveplan/packages/security-stack/security/hibp/src/range/range-strict-frame.ts
2026-05-27 23:47:53 +01:00

62 lines
1.9 KiB
TypeScript

import { HIBP_CODES } from "../error-codes.js";
import { HibpError } from "../errors/hibp-error.js";
/** Strict-mode framing: uppercase hex, single colon, decimal count. */
const STRICT_LINE_RE = /^[0-9A-F]{35}:[0-9]+$/;
/**
* One parsed line from a range response: the 35-char hex suffix plus its
* documented breach count.
*/
export interface RangeLine {
readonly suffix: string;
readonly count: number;
}
/**
* Validate and split a raw line. In strict mode, malformed lines throw
* `HIBP.MALFORMED_RESPONSE` with the offending zero-based line index. In
* off-mode, lowercase hex is accepted (uppercased on the way out), and an
* unparseable count defaults to `0`.
*/
export function parseRangeLine(
raw: string,
lineIndex: number,
strict: boolean
): RangeLine {
if (strict) {
if (!STRICT_LINE_RE.test(raw)) {
throw new HibpError(
HIBP_CODES.MALFORMED_RESPONSE,
`malformed range line at index ${lineIndex}`,
{ context: { lineIndex } }
);
}
const idx = raw.indexOf(":");
return {
suffix: raw.slice(0, idx),
count: Number.parseInt(raw.slice(idx + 1), 10),
};
}
const idx = raw.indexOf(":");
if (idx === -1) {
throw new HibpError(
HIBP_CODES.MALFORMED_RESPONSE,
`malformed range line at index ${lineIndex}`,
{ context: { lineIndex } }
);
}
const suffix = raw.slice(0, idx).toUpperCase();
if (suffix.length !== 35 || !/^[0-9A-F]{35}$/.test(suffix)) {
throw new HibpError(
HIBP_CODES.MALFORMED_RESPONSE,
`malformed range line at index ${lineIndex}`,
{ context: { lineIndex } }
);
}
const tail = raw.slice(idx + 1);
const parsed = Number.parseInt(tail, 10);
const count = Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
return { suffix, count };
}