(function initProtectionModel(root, factory) { const api = factory(); if (typeof module === 'object' && module.exports) module.exports = api; if (root) root.chimeraProtectionModel = api; })(typeof window !== 'undefined' ? window : globalThis, function createProtectionModel() { 'use strict'; const list = value => Array.isArray(value) ? value : []; const value = input => input == null || input === '' ? null : input; const first = (...inputs) => inputs.map(value).find(item => item != null) ?? null; const lower = input => String(input || '').trim().toLowerCase(); const missingStates = new Set(['missing', 'unavailable', 'disconnected', 'offline', 'not_mounted', 'absent']); const failedIntegrityStates = new Set(['failed', 'corrupt', 'mismatch', 'invalid']); function normalizeCopy(title, copy, index) { const volume = copy.volume || {}; const device = copy.device || volume.device || copy.storageDevice || {}; const id = first(copy.id, copy.copyId, copy.copy_id); const volumeId = first(copy.volumeId, copy.volume_id, volume.id); const deviceId = first(copy.deviceId, copy.device_id, device.id, volume.deviceId, volume.device_id); const deviceFingerprint = first(copy.deviceFingerprint, copy.stableFingerprint, device.stableFingerprint, device.stable_fingerprint); const volumeKey = first(copy.stableVolumeKey, copy.stable_volume_key, volume.stableVolumeKey, volume.stable_volume_key); const deviceKey = deviceId != null ? `device:${deviceId}` : deviceFingerprint ? `fingerprint:${deviceFingerprint}` : 'device:unknown'; const relativePath = first(copy.relativePath, copy.relative_path, copy.path, ''); const mountPath = first(copy.mountPath, copy.mount_path, volume.mountPath, volume.mount_path, ''); const fullPath = mountPath && relativePath ? `${String(mountPath).replace(/[\\/]+$/, '')}/${String(relativePath).replace(/^[\\/]+/, '')}` : String(first(relativePath, mountPath, 'Unknown path')); const availabilityState = lower(first(copy.availabilityState, copy.availability_state, volume.availabilityState, volume.availability_state, device.availabilityState, device.availability_state, 'unknown')); const integrityState = lower(first(copy.integrityState, copy.integrity_state, 'unknown')); const hashState = lower(first(copy.hashState, copy.hash_state, copy.inventoryHash || copy.inventory_hash ? 'recorded' : 'missing')); const missing = missingStates.has(availabilityState); const integrityFailed = failedIntegrityStates.has(integrityState); return Object.freeze({ copyId: id != null ? String(id) : `unidentified-${index + 1}`, titleId: String(first(title.game_id, title.titleId, title.title_id, title.id, 'unknown')), titleName: String(first(title.prospero_name, title.name, title.title, title.game_id, 'Unknown title')), region: String(first(title.prospero_region, title.region, '')), deviceId: deviceId == null ? null : String(deviceId), deviceKey, deviceName: String(first(copy.deviceName, copy.displayName, device.displayName, device.display_name, deviceFingerprint, 'Unknown device')), deviceFingerprint: deviceFingerprint == null ? null : String(deviceFingerprint), volumeId: volumeId == null ? null : String(volumeId), volumeKey: volumeKey == null ? null : String(volumeKey), path: fullPath, sizeBytes: Number(first(copy.sizeBytes, copy.size_bytes, 0)) || 0, hash: first(copy.inventoryHash, copy.inventory_hash), hashAlgorithm: String(first(copy.hashAlgorithm, copy.hash_algorithm, 'sha256')), hashState, integrityState, lastVerifiedAt: first(copy.lastVerifiedAt, copy.last_verified_at), lastSeenAt: first(copy.lastSeenAt, copy.last_seen_at), availabilityState, missing, integrityFailed, restoreTarget: Object.freeze({ copyId: id != null ? String(id) : `unidentified-${index + 1}`, titleId: String(first(title.game_id, title.titleId, title.title_id, title.id, 'unknown')), volumeId: volumeId == null ? null : String(volumeId), deviceId: deviceId == null ? null : String(deviceId), path: fullPath }) }); } function evaluate(copies, requiredDistinctDevices = 2) { const concrete = list(copies); const healthyCopies = concrete.filter(copy => !copy.missing && !copy.integrityFailed); const knownDevices = new Set(healthyCopies.map(copy => copy.deviceKey).filter(key => key !== 'device:unknown')); const unknownDeviceCopies = healthyCopies.filter(copy => copy.deviceKey === 'device:unknown').length; const distinctDevices = knownDevices.size + (unknownDeviceCopies ? 1 : 0); const required = Math.max(1, Number(requiredDistinctDevices) || 2); const missingCopies = concrete.filter(copy => copy.missing).length; const failedCopies = concrete.filter(copy => copy.integrityFailed).length; const healthy = distinctDevices >= required; const sameDeviceExcess = Math.max(0, healthyCopies.length - distinctDevices); const status = healthy ? 'healthy' : healthyCopies.length ? 'needs-diversity' : concrete.length ? 'degraded' : 'unprotected'; return Object.freeze({ copyCount: concrete.length, healthyCopyCount: healthyCopies.length, distinctDeviceCount: distinctDevices, requiredDistinctDevices: required, missingCopyCount: missingCopies, failedCopyCount: failedCopies, sameDeviceExcess, healthy, status }); } function flatten(titles, options = {}) { const result = []; for (const title of list(titles)) { const copies = list(title.localCopies).map((copy, index) => normalizeCopy(title, copy, index)); const policy = evaluate(copies, options.requiredDistinctDevices); copies.forEach(copy => result.push(Object.freeze({ ...copy, policy }))); } return result; } return Object.freeze({ normalizeCopy, evaluate, flatten }); });