*/ private const SENSITIVE_KEYS = [ 'password', 'password2', 'secret', 'token', 'access_token', 'refresh_token', 'id_token', 'authorization', 'api_key', 'client_secret', 'smtp_password', 'microsoft_shared_client_secret', 'email', 'to_email', ]; /** @var array */ private const CHANGE_VALUE_ALLOWLIST = [ 'active', 'status', 'default_tenant_id', 'default_role_id', 'default_department_id', 'app_locale', 'app_theme', 'app_theme_user', 'app_registration', 'app_primary_color', 'api_token_default_ttl_days', 'api_token_max_ttl_days', 'user_inactivity_deactivate_days', 'user_inactivity_delete_days', 'system_audit_enabled', 'system_audit_retention_days', ]; public function redactMetadata(array $metadata): array { return $this->redactArray($metadata); } /** * @return array{changed_fields: list, changes: array} */ public function buildChangeSet(array $before, array $after): array { $changedFields = []; $changes = []; $allFields = array_values(array_unique(array_merge(array_keys($before), array_keys($after)))); sort($allFields, SORT_STRING); foreach ($allFields as $field) { if (!is_string($field) || trim($field) === '') { continue; } $beforeValue = $before[$field] ?? null; $afterValue = $after[$field] ?? null; if ($this->valuesEqual($beforeValue, $afterValue)) { continue; } $changedFields[] = $field; if ($this->isAllowlistedChangeField($field)) { $changes[$field] = [ 'before' => $this->normalizeValue($beforeValue), 'after' => $this->normalizeValue($afterValue), ]; continue; } $changes[$field] = [ 'before' => '[REDACTED]', 'after' => '[REDACTED]', ]; } return [ 'changed_fields' => $changedFields, 'changes' => $changes, ]; } public function hashValue(string $value): string { return RequestContext::hashValue($value); } private function redactArray(array $data): array { $result = []; $index = 0; foreach ($data as $rawKey => $value) { if ($index >= self::MAX_ARRAY_ITEMS) { break; } $key = trim((string) $rawKey); if ($key === '') { continue; } if ($this->isSensitiveKey($key)) { $result[$key] = '[REDACTED]'; $index++; continue; } if (is_array($value)) { $result[$key] = $this->redactArray($value); $index++; continue; } $result[$key] = $this->normalizeValue($value); $index++; } return $result; } private function normalizeValue(mixed $value): mixed { if (is_null($value) || is_bool($value) || is_int($value) || is_float($value)) { return $value; } if (is_array($value)) { return $this->redactArray($value); } $string = trim((string) $value); if ($string === '') { return ''; } if (strlen($string) > self::MAX_STRING_LENGTH) { return substr($string, 0, self::MAX_STRING_LENGTH); } return $string; } private function valuesEqual(mixed $left, mixed $right): bool { if (is_array($left) || is_array($right)) { return json_encode($left) === json_encode($right); } return (string) $left === (string) $right; } private function isSensitiveKey(string $key): bool { $key = strtolower(trim($key)); if ($key === '') { return false; } if (in_array($key, self::SENSITIVE_KEYS, true)) { return true; } return str_contains($key, 'password') || str_contains($key, 'secret') || str_contains($key, 'token') || str_contains($key, 'authorization') || str_ends_with($key, '_key'); } private function isAllowlistedChangeField(string $field): bool { return in_array(strtolower(trim($field)), self::CHANGE_VALUE_ALLOWLIST, true); } }