Run the configured formatter across the repository before audit remediation so later security fixes produce reviewable, conflict-resistant diffs.
53 lines
1.7 KiB
TypeScript
53 lines
1.7 KiB
TypeScript
import crypto from 'node:crypto';
|
||
|
||
export class InvalidCursorError extends Error {
|
||
readonly code = 'INVALID_CURSOR';
|
||
constructor(message = 'Курсор синхронизации недействителен или повреждён') {
|
||
super(message);
|
||
this.name = 'InvalidCursorError';
|
||
}
|
||
}
|
||
|
||
const CURSOR_PREFIX = 'cur_';
|
||
const CURSOR_SALT = 'krona_staff_sync_cursor_salt_v1';
|
||
|
||
export function encodeCursor(tenantId: string, seq: bigint | number | string): string {
|
||
const seqStr = String(seq);
|
||
const hash = crypto
|
||
.createHash('sha256')
|
||
.update(`${tenantId}:${seqStr}:${CURSOR_SALT}`)
|
||
.digest('hex')
|
||
.slice(0, 12);
|
||
return `${CURSOR_PREFIX}${seqStr}_${hash}`;
|
||
}
|
||
|
||
export function decodeCursor(tenantId: string, cursor: string): bigint {
|
||
if (typeof cursor !== 'string' || !cursor) {
|
||
throw new InvalidCursorError('Курсор синхронизации не задан');
|
||
}
|
||
const match = cursor.match(/^cur_(\d+)_([0-9a-f]{12})$/);
|
||
if (!match) {
|
||
throw new InvalidCursorError('Неверный формат курсора синхронизации');
|
||
}
|
||
const seqStr = match[1]!;
|
||
const candidateHash = match[2]!;
|
||
const expectedHash = crypto
|
||
.createHash('sha256')
|
||
.update(`${tenantId}:${seqStr}:${CURSOR_SALT}`)
|
||
.digest('hex')
|
||
.slice(0, 12);
|
||
|
||
const candidateBuffer = Buffer.from(candidateHash, 'utf8');
|
||
const expectedBuffer = Buffer.from(expectedHash, 'utf8');
|
||
|
||
if (
|
||
candidateBuffer.length !== expectedBuffer.length ||
|
||
!crypto.timingSafeEqual(candidateBuffer, expectedBuffer)
|
||
) {
|
||
throw new InvalidCursorError(
|
||
'Подпись курсора синхронизации недействительна для данного арендатора',
|
||
);
|
||
}
|
||
|
||
return BigInt(seqStr);
|
||
}
|