import { createHmac, randomUUID, timingSafeEqual } from 'node:crypto';
const ROOT_TOKEN_PREFIX = 'frt1';
const DELETE_PREVIEW_TOKEN_PREFIX = 'fdt1';
export class FileRootTokenError extends Error {
    code = 'invalid_root_token';
    constructor() {
        super('Invalid file root token');
    }
}
export class FileDeletePreviewTokenError extends Error {
    code = 'invalid_delete_preview_token';
    constructor() {
        super('Invalid file delete preview token');
    }
}
export function signFileRootToken(config, options) {
    const issuedAt = (options.now ?? (() => new Date()))().toISOString();
    const payload = {
        version: 1,
        terminalId: options.terminalId,
        rootPath: options.rootPath,
        issuedAt,
        nonce: (options.nonce ?? randomUUID)()
    };
    return {
        rootToken: signToken(ROOT_TOKEN_PREFIX, config.sessionSecret, payload),
        issuedAt
    };
}
export function verifyFileRootToken(config, token) {
    const payload = verifyToken(config.sessionSecret, token, ROOT_TOKEN_PREFIX, () => new FileRootTokenError());
    if (!isFileRootTokenPayload(payload)) {
        throw new FileRootTokenError();
    }
    return payload;
}
export function signFileDeletePreviewToken(config, options) {
    const payload = {
        version: 1,
        rootTokenHash: tokenHash(config.sessionSecret, options.rootToken),
        path: options.path,
        kind: options.kind,
        descendantCount: options.descendantCount,
        entryVersion: options.entryVersion,
        issuedAt: (options.now ?? (() => new Date()))().toISOString(),
        nonce: (options.nonce ?? randomUUID)()
    };
    return signToken(DELETE_PREVIEW_TOKEN_PREFIX, config.sessionSecret, payload);
}
export function verifyFileDeletePreviewToken(config, token, rootToken, expected) {
    const payload = verifyToken(config.sessionSecret, token, DELETE_PREVIEW_TOKEN_PREFIX, () => new FileDeletePreviewTokenError());
    if (!isFileDeletePreviewTokenPayload(payload)) {
        throw new FileDeletePreviewTokenError();
    }
    if (payload.rootTokenHash !== tokenHash(config.sessionSecret, rootToken) ||
        payload.path !== expected.path ||
        payload.kind !== expected.kind ||
        payload.descendantCount !== expected.descendantCount ||
        !sameFileVersion(payload.entryVersion, expected.entryVersion)) {
        throw new FileDeletePreviewTokenError();
    }
}
function signToken(prefix, secret, payload) {
    const payloadSegment = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
    const signature = signatureSegment(secret, `${prefix}.${payloadSegment}`);
    return `${prefix}.${payloadSegment}.${signature}`;
}
function verifyToken(secret, token, expectedPrefix, error) {
    const parts = token.split('.');
    if (parts.length !== 3 || parts[0] !== expectedPrefix || !parts[1] || !parts[2]) {
        throw error();
    }
    const signedValue = `${parts[0]}.${parts[1]}`;
    const expectedSignature = signatureSegment(secret, signedValue);
    if (!timingSafeEqualString(parts[2], expectedSignature)) {
        throw error();
    }
    try {
        return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
    }
    catch {
        throw error();
    }
}
function signatureSegment(secret, value) {
    return createHmac('sha256', secret).update(value).digest('base64url');
}
function tokenHash(secret, token) {
    return createHmac('sha256', secret).update(token).digest('base64url');
}
function timingSafeEqualString(actual, expected) {
    const actualBuffer = Buffer.from(actual, 'base64url');
    const expectedBuffer = Buffer.from(expected, 'base64url');
    return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
}
function isFileRootTokenPayload(value) {
    if (!value || typeof value !== 'object') {
        return false;
    }
    const payload = value;
    return (payload.version === 1 &&
        typeof payload.terminalId === 'string' &&
        payload.terminalId.length > 0 &&
        typeof payload.rootPath === 'string' &&
        payload.rootPath.length > 0 &&
        typeof payload.issuedAt === 'string' &&
        typeof payload.nonce === 'string' &&
        payload.nonce.length > 0);
}
function isFileDeletePreviewTokenPayload(value) {
    if (!value || typeof value !== 'object') {
        return false;
    }
    const payload = value;
    return (payload.version === 1 &&
        typeof payload.rootTokenHash === 'string' &&
        typeof payload.path === 'string' &&
        typeof payload.kind === 'string' &&
        typeof payload.descendantCount === 'number' &&
        Number.isSafeInteger(payload.descendantCount) &&
        payload.descendantCount >= 0 &&
        isFileVersionPayload(payload.entryVersion) &&
        typeof payload.issuedAt === 'string' &&
        typeof payload.nonce === 'string' &&
        payload.nonce.length > 0);
}
function isFileVersionPayload(value) {
    if (!value || typeof value !== 'object') {
        return false;
    }
    const version = value;
    return (typeof version.size === 'number' &&
        typeof version.mtimeMs === 'number' &&
        (version.ino === undefined || typeof version.ino === 'number'));
}
function sameFileVersion(left, right) {
    return left.size === right.size && left.mtimeMs === right.mtimeMs && (right.ino === undefined || left.ino === right.ino);
}
//# sourceMappingURL=rootToken.js.map