diff --git a/src/models/backup.model.ts b/src/models/backup.model.ts index 5050688..27f06b0 100644 --- a/src/models/backup.model.ts +++ b/src/models/backup.model.ts @@ -1,7 +1,304 @@ import { Device, Process, SecretsStore } from "pkg/sdk_client"; -export interface BackUp { - device: Device, - secrets: SecretsStore, - processes: Record, +export interface UserDataBackUp { + version: number, + exported_at: Date, + user_data: { + device: Device | string, // string if encrypted + secrets: SecretsStore | string, // string if encrypted + processes: Record | string, // string if encrypted + }, + metadata: { + encrypted: boolean, + checksum: string, + } +} + +/** + * Validates a UserDataBackUp object + * @param backup - The backup object to validate + * @returns null if valid, error message string if invalid + */ +export function isValid(backup: any): string | null { + // Check if backup is an object + if (!backup || typeof backup !== 'object') { + return 'Backup must be a valid object'; + } + + // Validate version + if (typeof backup.version !== 'number') { + return 'Version must be a number'; + } + if (backup.version <= 0) { + return 'Version must be greater than 0'; + } + + // Validate exported_at + if (!(backup.exported_at instanceof Date)) { + return 'exported_at must be a valid Date object'; + } + if (isNaN(backup.exported_at.getTime())) { + return 'exported_at must be a valid date'; + } + + // Validate user_data + if (!backup.user_data || typeof backup.user_data !== 'object') { + return 'user_data must be a valid object'; + } + + // Validate metadata + if (!backup.metadata || typeof backup.metadata !== 'object') { + return 'metadata must be a valid object'; + } + + if (typeof backup.metadata.encrypted !== 'boolean') { + return 'metadata.encrypted must be a boolean'; + } + + if (typeof backup.metadata.checksum !== 'string') { + return 'metadata.checksum must be a string'; + } + if (backup.metadata.checksum.length === 0) { + return 'metadata.checksum cannot be empty'; + } + + // Validate user_data fields based on encryption status + const { encrypted } = backup.metadata; + const { device, secrets, processes } = backup.user_data; + + // Validate device + if (encrypted) { + if (typeof device !== 'string') { + return 'device must be a string when backup is encrypted'; + } + if (device.length === 0) { + return 'device cannot be empty when backup is encrypted'; + } + } else { + if (!device || typeof device !== 'object') { + return 'device must be a valid object when backup is not encrypted'; + } + // Validate Device structure + const deviceError = validateDevice(device); + if (deviceError) { + return `Invalid device: ${deviceError}`; + } + } + + // Validate secrets + if (encrypted) { + if (typeof secrets !== 'string') { + return 'secrets must be a string when backup is encrypted'; + } + if (secrets.length === 0) { + return 'secrets cannot be empty when backup is encrypted'; + } + } else { + if (!secrets || typeof secrets !== 'object') { + return 'secrets must be a valid object when backup is not encrypted'; + } + // Validate SecretsStore structure + const secretsError = validateSecretsStore(secrets); + if (secretsError) { + return `Invalid secrets: ${secretsError}`; + } + } + + // Validate processes + if (encrypted) { + if (typeof processes !== 'string') { + return 'processes must be a string when backup is encrypted'; + } + if (processes.length === 0) { + return 'processes cannot be empty when backup is encrypted'; + } + } else { + if (!processes || typeof processes !== 'object') { + return 'processes must be a valid object when backup is not encrypted'; + } + // Validate processes Record structure + const processesError = validateProcessesRecord(processes); + if (processesError) { + return `Invalid processes: ${processesError}`; + } + } + + return null; // Valid +} + +/** + * Validates a Device object + */ +function validateDevice(device: any): string | null { + if (!device || typeof device !== 'object') { + return 'Device must be a valid object'; + } + + // Check for required Device properties + if (!device.sp_wallet || typeof device.sp_wallet !== 'object') { + return 'sp_wallet must be a valid object'; + } + + // Validate sp_wallet structure (basic validation) + if (!device.sp_wallet.sp_address || typeof device.sp_wallet.sp_address !== 'string') { + return 'sp_wallet.sp_address must be a non-empty string'; + } + + // pairing_process_commitment can be null or an object + if (device.pairing_process_commitment !== null && + (typeof device.pairing_process_commitment !== 'object' || + typeof device.pairing_process_commitment.txid !== 'string' || + typeof device.pairing_process_commitment.vout !== 'number')) { + return 'pairing_process_commitment must be null or a valid OutPoint object with txid (string) and vout (number)'; + } + + // Validate paired_member + if (!device.paired_member || typeof device.paired_member !== 'object') { + return 'paired_member must be a valid object'; + } + + if (!Array.isArray(device.paired_member.sp_addresses)) { + return 'paired_member.sp_addresses must be an array'; + } + + if (!device.paired_member.sp_addresses.every((addr: any) => typeof addr === 'string')) { + return 'All sp_addresses must be strings'; + } + + return null; // Valid +} + +/** + * Validates a SecretsStore object + */ +function validateSecretsStore(secrets: any): string | null { + if (!secrets || typeof secrets !== 'object') { + return 'Secrets must be a valid object'; + } + + // Validate shared_secrets + if (!secrets.shared_secrets || typeof secrets.shared_secrets !== 'object') { + return 'shared_secrets must be a valid object'; + } + + // Validate unconfirmed_secrets + if (!Array.isArray(secrets.unconfirmed_secrets)) { + return 'unconfirmed_secrets must be an array'; + } + + // Basic validation of shared_secrets entries + for (const [key, value] of Object.entries(secrets.shared_secrets)) { + if (typeof key !== 'string') { + return 'All shared_secrets keys must be strings'; + } + if (typeof value !== 'string') { + return 'All shared_secrets values must be strings'; + } + } + + // Basic validation of unconfirmed_secrets entries + for (const secret of secrets.unconfirmed_secrets) { + if (typeof secret !== 'string') { + return 'All unconfirmed_secrets must be strings'; + } + } + + return null; // Valid +} + +/** + * Validates a Record object + */ +function validateProcessesRecord(processes: any): string | null { + if (!processes || typeof processes !== 'object') { + return 'Processes must be a valid object'; + } + + for (const [key, process] of Object.entries(processes)) { + if (typeof key !== 'string') { + return 'All process keys must be strings'; + } + const processError = validateProcess(process); + if (processError) { + return `Invalid process '${key}': ${processError}`; + } + } + + return null; // Valid +} + +/** + * Validates a Process object + */ +function validateProcess(process: any): string | null { + if (!process || typeof process !== 'object') { + return 'Process must be a valid object'; + } + + if (!Array.isArray(process.states)) { + return 'Process states must be an array'; + } + + // Validate each state in the process + for (let i = 0; i < process.states.length; i++) { + const stateError = validateProcessState(process.states[i]); + if (stateError) { + return `Invalid state at index ${i}: ${stateError}`; + } + } + + return null; // Valid +} + +/** + * Validates a ProcessState object + */ +function validateProcessState(state: any): string | null { + if (!state || typeof state !== 'object') { + return 'Process state must be a valid object'; + } + + // Validate commited_in (OutPoint) + if (!state.commited_in || typeof state.commited_in !== 'object') { + return 'commited_in must be a valid object'; + } + + if (typeof state.commited_in.txid !== 'string') { + return 'commited_in.txid must be a string'; + } + if (typeof state.commited_in.vout !== 'number') { + return 'commited_in.vout must be a number'; + } + + // Validate pcd_commitment + if (!state.pcd_commitment || typeof state.pcd_commitment !== 'object') { + return 'pcd_commitment must be a valid object'; + } + + // Validate state_id + if (typeof state.state_id !== 'string') { + return 'state_id must be a string'; + } + + // Validate keys + if (!state.keys || typeof state.keys !== 'object') { + return 'keys must be a valid object'; + } + + // Validate validation_tokens + if (!Array.isArray(state.validation_tokens)) { + return 'validation_tokens must be an array'; + } + + // Validate public_data + if (!state.public_data || typeof state.public_data !== 'object') { + return 'public_data must be a valid object'; + } + + // Validate roles + if (!state.roles || typeof state.roles !== 'object') { + return 'roles must be a valid object'; + } + + return null; // Valid }