const ALGORITHM = 'AES-GCM';
const IV_LENGTH = 12;
const TAG_LENGTH = 128;
const KEY_BYTES = 32; // AES-256 requires exactly 32 bytes
const KEY_HEX_LENGTH = KEY_BYTES * 2;
const HEX_RE = /^[0-9a-fA-F]+$/;
/**
 * Thrown for invalid `CMS_ENCRYPTION_KEY` shape. We use a named class so the
 * caller (typically an API handler) can map it to a clear 500 with operator
 * guidance rather than the opaque `OperationError` that some Web Crypto
 * implementations throw on malformed keys.
 */
export class InvalidEncryptionKeyError extends Error {
    constructor(reason) {
        super(`[Actuate CMS] CMS_ENCRYPTION_KEY is invalid: ${reason}. ` +
            `Generate a valid key with: ` +
            `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`);
        this.name = 'InvalidEncryptionKeyError';
    }
}
/**
 * Validate that `keyHex` is exactly 64 hex chars (32 bytes for AES-256).
 *
 * Without this guard, WebCrypto's `importKey` either throws an opaque
 * `OperationError` (short keys), or in some non-spec runtimes silently
 * truncates / pads — both produce ciphertext that the matching `decrypt`
 * call can't reverse, surfaced to the user as a generic "decryption failed"
 * weeks after the bad key was first deployed.
 */
function validateKey(keyHex) {
    if (typeof keyHex !== 'string' || keyHex.length === 0) {
        throw new InvalidEncryptionKeyError('key is empty or not a string');
    }
    if (keyHex.length !== KEY_HEX_LENGTH) {
        throw new InvalidEncryptionKeyError(`key must be ${KEY_HEX_LENGTH} hex characters (${KEY_BYTES} bytes), got ${keyHex.length}`);
    }
    if (!HEX_RE.test(keyHex)) {
        throw new InvalidEncryptionKeyError('key must contain only hex characters [0-9a-fA-F]');
    }
}
/** Encrypt a field value using AES-256-GCM. */
export async function encryptField(value, keyHex) {
    validateKey(keyHex);
    const key = await importKey(keyHex);
    const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
    const encoded = new TextEncoder().encode(value);
    const ciphertext = await crypto.subtle.encrypt({ name: ALGORITHM, iv, tagLength: TAG_LENGTH }, key, encoded);
    const combined = new Uint8Array(iv.length + ciphertext.byteLength);
    combined.set(iv);
    combined.set(new Uint8Array(ciphertext), iv.length);
    return bufferToHex(combined);
}
/** Decrypt a field value encrypted with AES-256-GCM. */
export async function decryptField(encrypted, keyHex) {
    validateKey(keyHex);
    const key = await importKey(keyHex);
    const data = hexToBuffer(encrypted);
    const iv = data.slice(0, IV_LENGTH);
    const ciphertext = data.slice(IV_LENGTH);
    const decrypted = await crypto.subtle.decrypt({ name: ALGORITHM, iv, tagLength: TAG_LENGTH }, key, ciphertext);
    return new TextDecoder().decode(decrypted);
}
async function importKey(keyHex) {
    const keyData = hexToBuffer(keyHex);
    return crypto.subtle.importKey('raw', keyData, ALGORITHM, false, [
        'encrypt',
        'decrypt',
    ]);
}
function bufferToHex(buffer) {
    return Array.from(buffer)
        .map((b) => b.toString(16).padStart(2, '0'))
        .join('');
}
/**
 * Decode a hex string to bytes. Caller is expected to have validated shape
 * via `validateKey` for keys; this function tolerates already-validated input
 * and rejects odd-length hex (which previously silently dropped trailing chars).
 */
function hexToBuffer(hex) {
    if (hex.length % 2 !== 0) {
        throw new Error('hex string must have an even number of characters');
    }
    const bytes = new Uint8Array(hex.length / 2);
    for (let i = 0; i < hex.length; i += 2) {
        const byte = parseInt(hex.slice(i, i + 2), 16);
        if (Number.isNaN(byte)) {
            throw new Error(`invalid hex character at offset ${i}`);
        }
        bytes[i / 2] = byte;
    }
    return bytes;
}
//# sourceMappingURL=encrypted-fields.js.map